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

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

相关推荐
长安er14 小时前
LeetCode 20/155/394/739/84/42/单调栈核心原理与经典题型全解析
数据结构·算法·leetcode·动态规划·
MarkHD14 小时前
智能体在车联网中的应用:第28天 深度强化学习实战:从原理到实现——掌握近端策略优化(PPO)算法
算法
能源系统预测和优化研究14 小时前
【原创代码改进】考虑共享储能接入的工业园区多类型负荷需求响应经济运行研究
大数据·算法
yoke菜籽14 小时前
LeetCode——三指针
算法·leetcode·职场和发展
小高不明15 小时前
前缀和一维/二维-复习篇
开发语言·算法
bin915315 小时前
当AI优化搜索引擎算法:Go初级开发者的创意突围实战指南
人工智能·算法·搜索引擎·工具·ai工具
曹牧16 小时前
Java:Math.abs()‌
java·开发语言·算法
天才测试猿16 小时前
2026全新软件测试面试八股文【含答案+文档】
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
CoovallyAIHub17 小时前
纯视觉的终结?顶会趋势:不会联觉(多模态)的CV不是好AI
深度学习·算法·计算机视觉
CoovallyAIHub17 小时前
一文读懂大语言模型家族:LLM、MLLM、LMM、VLM核心概念全解析
深度学习·算法·计算机视觉