更新题目 55. Jump Game
题目 lc55 · 修改(rev 2) · · 2026-10-27 00:34
body
@@ -1,7 +1,10 @@ ## 题目 -`nums[i]` 是在位置 i 最多能往前跳几步。从位置 0 出发,问能不能到达最后一个位置。 +给一个非负整数数组 `nums`,`nums[i]` 是站在下标 i 时最多能往右跳几步(可以跳 1 到 `nums[i]` 步中的任意一种)。从下标 0 出发,问能不能到达最后一个下标。 -follow-up 是 LC45:保证能到达最后一个位置,问最少跳几次。 +- `[2, 3, 1, 1, 4]` 返回 True:从 0 跳 1 步到 1,再从 1 跳 3 步到 4。 +- `[3, 2, 1, 0, 4]` 返回 False:不管怎么跳都会停在下标 3,`nums[3] = 0`,过不去。 + +常见的 follow-up 是 LC45 Jump Game II:题目保证一定能到达最后一个下标,问最少跳几次。`[2, 3, 1, 1, 4]` 的答案是 2(0 → 1 → 4)。 [LC55](https://leetcode.com/problems/jump-game/) · [LC45](https://leetcode.com/problems/jump-game-ii/) @@ -9,5 +12,10 @@ ## 面经里怎么考的 -947513(backend NG,2022 Q4,Fail):先考 LC55,follow-up 是 LC45【推断:"舞舞""肆五"】。这类"看起来像 DP、其实贪心更好"的题和 GD 2025-03 那句评价的描述也对得上。 +- Visa:Senior Software Engineer 的 VO 第一轮(project 加 coding),coding 部分是 LC55 原题。这场面试楼主最后没拿到 offer,帖子的结果是整场的,不一定是这道题没答好。 +- Hive:backend NG(2022 Q4),先考 LC55,follow-up 是 LC45,原帖用谐音「舞舞」「肆五」写的题号。楼主这次面试挂了。 +- Temu:Payments backend 组的店面考 LC45,楼主没想到贪心。 +- Amazon:SDE II onsite 的 Round 2 DSA 考「跳跃游戏」,记录里没写是 55 还是 45。这场面试楼主最后也没拿到 offer,同样不一定是这道题的问题。 + +Hive 那篇是先考 LC55 再追问 LC45,Temu 直接考 LC45,所以两题要连着准备。Temu 那位卡在想不到贪心,面试时可以先说出 DP 做法,再讲怎么用贪心降到 $O(n)$(见 Follow-up 第一条)。 ## 思路 @@ -15,49 +23,82 @@ ### LC55:能不能跳到最后 -**贪心:**维护目前能到达的最远位置 reach,从左往右扫。如果当前位置 i 已经超过了 reach,说明根本走不到这里,返回 False;否则用 `i + nums[i]` 更新 reach。 +从左往右扫,维护一个变量 `farthest`,意思是「下标 0 到 i − 1 里所有能到达的位置,最远能跳到哪个下标」。扫到下标 i 时分两种情况: -**为什么贪心是对的:**能到达的位置一定是从 0 开始的一段连续区间。因为如果能从位置 i 跳到 j,那 i 和 j 之间的每个位置也都能跳到。所以只需要记住这段区间的右端点。 +- `i > farthest`:前面所有能到的位置都跳不到 i,i 到不了,i 后面的下标更到不了,直接返回 False。 +- `i <= farthest`:i 能到达,从 i 最远能跳到 `i + nums[i]`,用它更新 `farthest`。 + +扫完都没返回 False,说明每个下标都能到达,包括最后一个。 + +这样做对,是因为**能到达的下标一定是从 0 开始的一段连续区间**。站在下标 p 能跳 1 到 `nums[p]` 步,所以 p 到 `p + nums[p]` 之间的每个下标都能到。从 0 开始,每个能到的位置都把一段紧挨着的区间加进来,合起来始终是 `[0, farthest]` 这一段,中间不会有空洞。于是只需要记住区间的右端点 `farthest`,扫到第一个落在区间外的下标就说明卡住了。 + +拿 `[3, 2, 1, 0, 4]` 走一遍:i = 0 到 3 时 `farthest` 分别是 3、3、3、3,i = 4 时 `4 > 3`,返回 False。 + +> [!say] The set of reachable indices is always a prefix of the array, because from index i I can land on any index up to i plus nums[i]. So I only track the farthest reachable index. If I ever scan an index beyond it, that index is unreachable and I return false. ### LC45:最少跳几次 -**按 BFS 的层来理解:**第 0 层是位置 0;第 1 层是跳一次能到的所有位置;第 k 层是跳 k 次能到、但跳 k − 1 次到不了的位置。每一层也是一段连续区间,所以只需要记录当前这一层的右端点 cur_end,以及从这一层出发能到的最远位置 farthest。扫到 cur_end 时,当前这一层走完了,想继续往前就必须再跳一次,于是 steps 加 1,下一层的右端点变成 farthest。 +按 BFS 的层来看。第 0 层是下标 0。第 k 层是「最少要跳 k 次才能到」的下标。和 LC55 同样的道理,跳不超过 k 次能到的下标是一段从 0 开始的连续区间,所以每一层也是一段连续区间,紧接在上一层后面。 + +用两个变量记录层: + +- `layer_end`:当前这一层的右端点,也就是跳 `jumps` 次最远能到哪。 +- `next_layer_end`:从已经扫过的下标再跳一次,最远能到哪。这就是下一层的右端点。 + +从左往右扫,每个下标 i 先用 `i + nums[i]` 更新 `next_layer_end`。扫到 `i == layer_end` 时,当前这一层扫完了,终点还没到,必须再跳一次:`jumps` 加 1,`layer_end` 换成 `next_layer_end`。 + +拿 `[2, 3, 1, 1, 4]` 走一遍:i = 0 时 `next_layer_end` 变成 2,同时 i 等于 `layer_end`(0),换层,`jumps` 为 1,`layer_end` 为 2。i = 1 把 `next_layer_end` 推到 4。i = 2 又等于 `layer_end`,换层,`jumps` 为 2,`layer_end` 为 4。i = 3 不换层。下标 4 是终点,不扫,答案是 2。第 1 层是下标 1 到 2,第 2 层是下标 3 到 4。 + +这个答案是最少跳数,因为第 k 层的右端点就是跳 k 次能到的最远下标。终点第一次落进某一层时,那一层的编号就是最少跳数,跳更少的次数连那一层都到不了。 + +> [!say] For Jump Game II, I think of it as BFS by layers. Each layer is a contiguous range of indices reachable with the same minimum number of jumps. I track where the current layer ends and the farthest index reachable from it. When I reach the end of the current layer, I have to jump once more, and the next layer ends at that farthest index. It's O(n) time and O(1) space. ## 代码 ```python -def canJump(nums): - reach = 0 - for i, x in enumerate(nums): - if i > reach: # 走到了够不着的位置 - return False - reach = max(reach, i + x) - return True +from typing import List -def jump(nums): - steps = cur_end = farthest = 0 - for i in range(len(nums) - 1): - farthest = max(farthest, i + nums[i]) - if i == cur_end: # 当前这一跳能到的范围走完了,必须再跳一次 - steps += 1 - cur_end = farthest - return steps + +class Solution: + def canJump(self, nums: List[int]) -> bool: + """LC55:从下标 0 出发,能不能到达最后一个下标。""" + farthest = 0 # 目前能到达的最远下标 + for i, step in enumerate(nums): + if i > farthest: # 下标 i 够不着,后面的也够不着 + return False + farthest = max(farthest, i + step) + return True + + def jump(self, nums: List[int]) -> int: + """LC45:保证能到达最后一个下标,求最少跳几次。""" + last = len(nums) - 1 + jumps = 0 + layer_end = 0 # 跳 jumps 次能到达的最远下标 + next_layer_end = 0 # 再跳一次能到达的最远下标 + for i in range(last): # 不扫最后一个下标:到了就不用再跳 + next_layer_end = max(next_layer_end, i + nums[i]) + if i == layer_end: # 这一层扫完了,还没到终点,必须再跳一次 + jumps += 1 + layer_end = next_layer_end + return jumps ``` + +2026-10-27 用 Python 3.13 跑过: + +- 示例 `[2, 3, 1, 1, 4]`、`[3, 2, 1, 0, 4]`、`[0]`、`[1, 1]`、`[2, 0, 0]` 的结果分别是 True 和 2、False、True 和 0、True 和 1、True 和 1。 +- 和暴力解对拍:暴力解从下标 0 做 BFS,求出到最后一个下标的最少跳数,到不了就是 None。随机生成 20 万组长度 1 到 8、元素 0 到 4 的数组,`canJump` 和「暴力解不是 None」全部一致。其中 150600 组能到达,`jump` 和暴力解的跳数全部一致。 ## 复杂度 -两题都是时间 O(n)、空间 O(1)。 +两题都只扫一遍数组,时间 $O(n)$,额外空间 $O(1)$。 ## 易错点 -- **LC45 的循环只到 len(nums) − 2。**到达最后一个位置以后不需要再跳。如果循环包括最后一个位置,当 cur_end 恰好等于它时会多算一跳。 -- `[0]` 只有一个元素时,LC55 返回 True,LC45 返回 0。 +- **LC45 的循环不能扫到最后一个下标。**终点恰好是某一层的右端点时,扫到终点又会触发一次 `i == layer_end`,多加一跳。把循环改成 `range(len(nums))` 的错误版本,`[2, 3, 1, 1, 4]` 返回 3(正确是 2),`[0]` 返回 1(正确是 0)。在下面对拍用的 150600 组能到达的随机输入里,错误版本有 70547 组多算了 1 跳(2026-10-27 实测)。 +- **LC55 要先判断 `i > farthest`,再更新 `farthest`。**顺序反了,够不着的下标也会用自己的 `i + nums[i]` 去更新。`[0, 1]` 正确答案是 False,顺序反了的版本返回 True(2026-10-27 实测)。 +- **LC45 的代码依赖「一定能到达」这个前提。**`[0, 1]` 实际到不了,`jump` 却返回 1。面试官要是改成「到不了返回 −1」,就在 `i == layer_end` 时先判断:`next_layer_end <= i` 说明再跳一次也离不开下标 i,返回 −1。这个改法 2026-10-27 和暴力解在 20 万组随机输入上对拍过,结果全部一致。 ## Follow-up -- **为什么不用 DP:**dp[i] 表示到 i 的最少跳数,要从前面所有能跳到 i 的位置转移,O(n²)。可以先说出 DP,再说贪心把它降到 O(n)。 -- **输出具体的跳法:**在扫每一层时记下是哪个位置把 farthest 推到了最远,换层时把它记成这一跳的落脚点。 -- **LC1306 Jump Game III**(可以往左也可以往右跳,问能不能到值为 0 的位置):这时区间不连续了,改用 BFS 或 DFS。 - -## 英文讲法 - -> [!say] For Jump Game II, I think of it as BFS by layers. Each layer is a contiguous range of indices reachable with the same number of jumps. I track where the current layer ends and the farthest index reachable from it. When I reach the end of the current layer, I have to jump once more, and the next layer ends at that farthest index. It's O(n) time and O(1) space. +- **先想到 DP 怎么办【推断】。**Temu 那篇楼主没想到贪心,DP 是更容易先想到的做法。`dp[i]` 表示到下标 i 的最少跳数,要从前面所有能跳到 i 的下标转移过来,时间 $O(n^2)$。面试里可以先说出 DP,再说明能到的下标总是连续区间,每一层只要记右端点,扫一遍就够,降到 $O(n)$。 +- **输出具体的跳法【推断】。**扫的时候额外记下把 `next_layer_end` 推到最远的那个下标,每次换层就把它记成这一跳的起跳点,最后补上终点。从这个下标起跳,下一层能到得最远,所以跳数等于 `jump()` 的结果,时间仍是 $O(n)$。2026-10-27 按这个做法写了代码,在约 15 万组能到达的随机输入上检查过:路径从 0 开始、到终点结束,每一跳不超过起跳点的 `nums` 值,跳数和暴力解一致。 +- **LC1306 Jump Game III【推断】。**每个下标往左或往右跳正好 `nums[i]` 步,问能不能到达值为 0 的下标。步数固定后能到的下标不再连续,「只记右端点」不成立,改成在下标上做 BFS 或 DFS,用 visited 数组去重,时间和空间都是 $O(n)$。
meta
@@ -1 +1,5 @@ -{} +{ + "depth_guide": "2026-10-23", + "rewritten_by": "cloud:20261026-2317", + "rewritten_at": "2026-10-27" +}