LeetCode 403 - 青蛙过河


文章目录

摘要

这道题看起来像是童话故事里的小游戏:一只青蛙要跳过一排石头,每次跳跃的距离有限制。

但实际上,它考察的是我们如何用动态规划(Dynamic Programming)或带记忆的搜索(DFS + Memoization)去推理所有可能的跳法。

它是那种"状态变化多、暴力不可行、但又有规律"的典型算法题。

解决它的关键,在于每块石头上能跳多远这件事,我们得能高效记录下来并复用结果。

描述

题目大意如下:

青蛙要从第一块石头跳到最后一块石头,石头的位置数组为 stones(升序排列)。

青蛙一开始在第 0 块石头上,第一步必须跳 1 个单位。

之后的每一步跳跃距离只能是上一步的 k-1kk+1,而且不能跳进水里。

例如:

txt 复制代码
输入:stones = [0,1,3,5,6,8,12,17]
输出:true

跳法如下:

  • 跳 1 → 到 1
  • 跳 2 → 到 3
  • 跳 2 → 到 5
  • 跳 3 → 到 8
  • 跳 4 → 到 12
  • 跳 5 → 到 17

另一组不成功的例子:

txt 复制代码
输入:stones = [0,1,2,3,4,8,9,11]
输出:false

因为中间出现了太大的空隙,青蛙根本跳不过去。

题解答案

如果我们尝试暴力递归,每次都枚举下一步能跳到哪,复杂度会爆炸。

因此,最佳解法是用 动态规划(DP)+ 哈希表记忆

思路如下:

  1. 我们用一个 Dictionary<Int, Set<Int>> 来记录:

    每块石头位置 stone,它可以通过哪些步长 k 跳到。

  2. 初始化:第 0 块石头只有步长 1 可选。

  3. 遍历每块石头:

    • 对于它能到达的所有步长 k
    • 看能不能跳到下一个石头 stone + k
    • 如果能跳到,就更新对应石头的步长集合(k-1, k, k+1,只要 > 0)。
  4. 最后看最后一块石头的步长集合是否非空,如果能被到达则返回 true

题解代码分析

下面是完整的 Swift 实现,可直接在 Xcode 或 Playground 运行。

swift 复制代码
import Foundation

class Solution {
    func canCross(_ stones: [Int]) -> Bool {
        // 如果第二块不是 1,青蛙第一步就跳不过去
        if stones[1] != 1 { return false }

        // 用字典记录每个石头能通过哪些步长到达
        var dp: [Int: Set<Int>] = [:]
        for stone in stones {
            dp[stone] = []
        }
        dp[0]?.insert(1)  // 起点的第一步固定为 1

        let stoneSet = Set(stones)  // 方便 O(1) 查找

        for stone in stones {
            guard let steps = dp[stone] else { continue }
            for k in steps {
                let next = stone + k
                if next == stones.last! {
                    return true
                }
                if stoneSet.contains(next) {
                    if k - 1 > 0 {
                        dp[next]?.insert(k - 1)
                    }
                    dp[next]?.insert(k)
                    dp[next]?.insert(k + 1)
                }
            }
        }

        return false
    }
}

// MARK: - 可运行 Demo
let solution = Solution()
print(solution.canCross([0,1,3,5,6,8,12,17])) // true
print(solution.canCross([0,1,2,3,4,8,9,11]))  // false

代码逻辑拆解:

  • dp[stone] 代表什么?

    表示青蛙"跳到这一块石头时",能使用哪些步长。

  • 为什么用 Set?

    因为可能有多种方式跳到同一块石头,比如不同路径带来不同步长。

  • 关键循环

    我们从每块石头出发,看它能跳多远,

    并把对应的步长更新到目标石头的集合中。

  • 提前结束优化

    一旦 next == stones.last!,说明已经能到终点,可以立即返回 true

  • 剪枝条件

    如果第二块不是 1,青蛙第一步就没法跳过去,直接返回 false

示例测试及结果

我们运行几个例子看看:

swift 复制代码
let solution = Solution()

print(solution.canCross([0,1,3,5,6,8,12,17])) // true
print(solution.canCross([0,1,2,3,4,8,9,11]))  // false
print(solution.canCross([0,1]))               // true
print(solution.canCross([0,2]))               // false
print(solution.canCross([0,1,3,6,10,13,15,18])) // true

输出结果如下:

txt 复制代码
true
false
true
false
true

完全符合预期。

时间复杂度

  • 外层遍历所有石头(n 次),
  • 内层对每块石头可能的跳法集合进行遍历。
    由于每个石头的步长数有限(最多几十个),整体近似为 O(n²)
    n <= 2000 的范围内可接受。

空间复杂度

  • 我们用了一个字典,每个键对应一个步长集合。
    所以空间复杂度为 O(n²)(在极端情况下,所有石头都可达)。

总结

这题是动态规划与哈希结合的典型代表:

你必须同时追踪"到达哪"和"怎么到达",仅靠一个维度的信息不够。

要点总结:

  • 状态转移依赖于上一步的跳距;
  • 哈希表高效地管理每个石头可达的步长;
  • 用 Set 去重可避免重复状态;
  • 提前终止和剪枝可以显著提升性能。

在真实业务中,这类"状态依赖上一步动作"的模型也常见,比如:

  • 游戏 AI 的路径判断
  • 物流系统中可行路线规划
  • 工作流任务依赖计算

所以别被"青蛙"骗了,其实这道题的思想可以直接迁移到复杂系统建模中。

相关推荐
地平线开发者4 小时前
三种 Badcase 精度验证方案详解与 hbm_infer 部署实录
算法·自动驾驶
papership4 小时前
【入门级-算法-5、数值处理算法:高精度的减法】
算法·1024程序员节
lingran__4 小时前
算法沉淀第十天(牛客2025秋季算法编程训练联赛2-基础组 和 奇怪的电梯)
c++·算法
DuHz4 小时前
基于MIMO FMCW雷达的二维角度分析多径抑制技术——论文阅读
论文阅读·物联网·算法·信息与通信·毫米波雷达
Dragon_D.5 小时前
排序算法大全——插入排序
算法·排序算法·c·学习方法
大数据张老师5 小时前
数据结构——红黑树
数据结构·算法·红黑树
Dream it possible!6 小时前
LeetCode 面试经典 150_链表_两数相加 (57_2_C++_中等)
leetcode·链表·面试
自在极意功。6 小时前
动态规划核心原理与高级实战:从入门到精通(Java全解)
java·算法·动态规划·最优子结构·重叠子问题
文火冰糖的硅基工坊6 小时前
[人工智能-大模型-54]:模型层技术 - 数据结构+算法 = 程序
数据结构·人工智能·算法