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 的路径判断
  • 物流系统中可行路线规划
  • 工作流任务依赖计算

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

相关推荐
CoovallyAIHub15 分钟前
MSD-DETR:面向机车弹簧检测的可变形注意力Detection Transformer
算法·架构
CoovallyAIHub20 分钟前
不改权重、不用训练!BEM用背景记忆抑制固定摄像头误检,YOLO/RT-DETR全系有效
算法·架构·github
Struggle_975525 分钟前
算法知识-从递归入手三维动态规划
算法·动态规划
yuan1999730 分钟前
使用模糊逻辑算法进行路径规划(MATLAB实现)
开发语言·算法·matlab
不才小强34 分钟前
线性表详解:顺序与链式存储
数据结构·算法
CoovallyAIHub34 分钟前
上交+阿里 | Interactive ASR:Agent框架做语音识别交互纠错,1轮交互语义错误率降57%
算法·架构·github
Aaron15881 小时前
8通道测向系统演示科研套件
人工智能·算法·fpga开发·硬件工程·信息与通信·信号处理·基带工程
计算机安禾1 小时前
【数据结构与算法】第42篇:并查集(Disjoint Set Union)
c语言·数据结构·c++·算法·链表·排序算法·深度优先
吃着火锅x唱着歌1 小时前
LeetCode 150.逆波兰表达式求值
linux·算法·leetcode
YuanDaima20481 小时前
二分查找基础原理与题目说明
开发语言·数据结构·人工智能·笔记·python·算法