题目描述
思路解析
一句话答案:LeetCode 403 青蛙过河:判断能否按『上一跳 k、这跳只能 k−1/k/k+1』落到最后一块石子。用状态带上一跳步长的哈希记忆化 DP,每块石子记下能落到它的步长集合往后递推,时间空间都是 O(n²)。
青蛙过河这道题到底在判什么
给一排升序的石子位置 stones,青蛙从第一块出发,第一跳必须是 1 个单位。之后若上一跳跨了 k,这一跳只能跨 k−1、k 或 k+1(步长为正),且每跳都要正好落在石子上,问能不能跳到最后一块。以 stones=[0,1,3,5,6,8,12,17] 为例存在一条可达路径,答案 true。
为什么把所有跳法枚举一遍会炸
每落到一块石子,下一跳在 k−1、k、k+1 三个步长里分岔,一路乘下去是 3 的石子数次方级的路径,石子一多就枚举不完。慢还慢在同一种局面被反复重算:站在石子 6、上一跳步长同为 3,往后那截路是定死的,暴力递归每碰一次却从头再跳。把这类局面结果记下复用,指数级枚举才压得平。
站在同一块石子,为什么还得记住上一跳跳多远
只知道青蛙站在石子 6 不够——它能跳多远得看上一跳跨了几格:上一跳是 1,这跳只能是 1、2;上一跳是 3,这跳能是 2、3、4。同一块石子用不同步长跳上来,往后能走的路就不一样,所以完整的局面必须是『石子位置 + 上一跳步长』这一对。
定成这一对,就满足无后效性(未来只由这对状态决定):知道『站在 6、上一跳步长 3』,能不能过河就定死了。于是用哈希表(按键直接查值的表)jumps 记账,键是石子位置 x,值是能落到 x 时上一跳的所有可能步长。
每块石子的步长集合怎么往后推
起手把 jumps[0] 设成 {0}:从 0 枚举 0−1、0、0+1 只有 0+1=1 为正,逼出唯一合法的首跳步长 1。之后从左到右扫每块石子 x,取出 jumps[x] 里每个步长 k,试 nk=k−1、k、k+1:只要 nk 为正、落点 x+nk 是石子,就把 nk 记进 jumps[x+nk]。落点是终点就返回能过河,扫完没到就是过不去。
拿题面示例把步长集合一块块填出来
拿题面的 stones=[0,1,3,5,6,8,12,17] 走一遍,先 jumps[0]={0}。从 0 步长 1 到 1、1 步长 2 到 3(站在石子 1、上一跳步长 1,所以这跳能取 k+1=2)、3 步长 2 与 3 到 5 和 6、5 步长 1 到 6,jumps[6] 集齐 {3,1}(3 来自石子 3 步长 3、1 来自石子 5 步长 1)。再往前推,石子 5 步长 3 到 8、石子 6 步长 2 到 8,jumps[8] 凑成 {3,2};8 步长 4 到 12、12 步长 5 到终点 17,返回 true。路径 0→1→3→5→8→12→17。
青蛙从石子 4 死活跳不到 8,这串为什么只能判 false
换 stones=[0,1,2,3,4,8,9,11]:青蛙能蹭到石子 4,可此时步长最多才 2、这跳最远跨 3,隔着 4 格的石子 8 够不着,jumps[8] 始终空着,返回 last==0 即过不去。每块石子步长集合 O(n) 个、各试 3 向,时间空间都 O(n²)。最易写错处:jumps[0] 忘塞 0,首跳无从触发;漏掉 nk 为正,青蛙往回跳;[0,1] 直达算 true,而 [0,2] 落不到 2 判 false。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住「jumps[x] 记到 x 的步长、每步尝试 k−1/k/k+1、落在石子就记下、碰到终点即 true」,下面逐石子套它。
初始化:jumps[0]={0}。这个「0」是技巧:它让第一跳的步长 nk=0+1=1 恰好满足「首跳为 1」的规则。
处理石子 0(紫):它的步长集合是 {0}。对每个 k,试着往前跳 k−1、k、k+1。
从 0 用步长 1 跳到石子 1(绿)。把步长 1 记进 jumps[1],以后从 1 还能继续往前。
处理石子 1(紫):它的步长集合是 {1}。对每个 k,试着往前跳 k−1、k、k+1。
从 1 用步长 1 会落到 2,但那里没有石子,这一跳不行。
从 1 用步长 2 跳到石子 3(绿)。把步长 2 记进 jumps[3],以后从 3 还能继续往前。
处理石子 3(紫):它的步长集合是 {2}。对每个 k,试着往前跳 k−1、k、k+1。
从 3 用步长 1 会落到 4,但那里没有石子,这一跳不行。
从 3 用步长 2 跳到石子 5(绿)。把步长 2 记进 jumps[5],以后从 5 还能继续往前。
从 3 用步长 3 跳到石子 6(绿)。把步长 3 记进 jumps[6],以后从 6 还能继续往前。
处理石子 5(紫):它的步长集合是 {2}。对每个 k,试着往前跳 k−1、k、k+1。
从 5 用步长 1 跳到石子 6(绿)。把步长 1 记进 jumps[6],以后从 6 还能继续往前。
从 5 用步长 2 会落到 7,但那里没有石子,这一跳不行。
从 5 用步长 3 跳到石子 8(绿)。把步长 3 记进 jumps[8],以后从 8 还能继续往前。
处理石子 6(紫):它的步长集合是 {1, 3}。对每个 k,试着往前跳 k−1、k、k+1。
从 6 用步长 1 会落到 7,但那里没有石子,这一跳不行。
从 6 用步长 2 跳到石子 8(绿)。把步长 2 记进 jumps[8],以后从 8 还能继续往前。
从 6 用步长 2 跳到石子 8(绿)。它的集合里已有 2。
从 6 用步长 3 会落到 9,但那里没有石子,这一跳不行。
从 6 用步长 4 会落到 10,但那里没有石子,这一跳不行。
处理石子 8(紫):它的步长集合是 {2, 3}。对每个 k,试着往前跳 k−1、k、k+1。
从 8 用步长 1 会落到 9,但那里没有石子,这一跳不行。
从 8 用步长 2 会落到 10,但那里没有石子,这一跳不行。
从 8 用步长 3 会落到 11,但那里没有石子,这一跳不行。
从 8 用步长 2 会落到 10,但那里没有石子,这一跳不行。
从 8 用步长 3 会落到 11,但那里没有石子,这一跳不行。
从 8 用步长 4 跳到石子 12(绿)。把步长 4 记进 jumps[12],以后从 12 还能继续往前。
处理石子 12(紫):它的步长集合是 {4}。对每个 k,试着往前跳 k−1、k、k+1。
从 12 用步长 3 会落到 15,但那里没有石子,这一跳不行。
从 12 用步长 4 会落到 16,但那里没有石子,这一跳不行。
从 12 用步长 5 正好跳到终点 17!青蛙过河成功,直接返回 true。
回放一条可行路径:0→1→3→5→8→12→17,每一跳的步长都满足「上一跳 ±1」的规则,最终落在终点 17。答案 true。
边界:[0,1] 直达 true;[0,2] 首跳不够 false;大缺口 false。
两个延伸:可记忆化 DFS(石子,步长)等价;状态必须含步长故非一维。
参考代码
from typing import Listfrom collections import defaultdictclass Solution: def canCross(self, stones: List[int]) -> bool: stone_set = set(stones) jumps = defaultdict(set) jumps[0].add(0) last = stones[-1] for x in stones: for k in list(jumps[x]): for nk in (k - 1, k, k + 1): if nk > 0 and x + nk in stone_set: if x + nk == last: return True jumps[x + nk].add(nk) return last == 0复杂度
- 时间:O(n²),n 是石子数。每块石子的步长集合最多 O(n) 个,每个步长试 3 个方向 O(1) 判石子;总体 O(n²)
- 空间:O(n²),jumps 表最坏每块石子存 O(n) 个步长,合计 O(n²);石子集合 O(n)
易错点
面试追问把动画讲成自己的话
追问能不能用记忆化 DFS(从位置 + 上一步长 出发)来解?
追问为什么本题不能用「能否到达每块石子」这种一维布尔 DP?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
最短公共超序列
LeetCode 1092 · 困难 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题