

文章目录
摘要
这道题看起来像是童话故事里的小游戏:一只青蛙要跳过一排石头,每次跳跃的距离有限制。
但实际上,它考察的是我们如何用动态规划(Dynamic Programming)或带记忆的搜索(DFS + Memoization)去推理所有可能的跳法。
它是那种"状态变化多、暴力不可行、但又有规律"的典型算法题。
解决它的关键,在于每块石头上能跳多远这件事,我们得能高效记录下来并复用结果。

描述
题目大意如下:
青蛙要从第一块石头跳到最后一块石头,石头的位置数组为 stones(升序排列)。
青蛙一开始在第 0 块石头上,第一步必须跳 1 个单位。
之后的每一步跳跃距离只能是上一步的 k-1、k 或 k+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)+ 哈希表记忆。
思路如下:
-
我们用一个
Dictionary<Int, Set<Int>>来记录:每块石头位置
stone,它可以通过哪些步长k跳到。 -
初始化:第 0 块石头只有步长
1可选。 -
遍历每块石头:
- 对于它能到达的所有步长
k, - 看能不能跳到下一个石头
stone + k。 - 如果能跳到,就更新对应石头的步长集合(
k-1, k, k+1,只要 > 0)。
- 对于它能到达的所有步长
-
最后看最后一块石头的步长集合是否非空,如果能被到达则返回
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 的路径判断
- 物流系统中可行路线规划
- 工作流任务依赖计算
所以别被"青蛙"骗了,其实这道题的思想可以直接迁移到复杂系统建模中。