当前:LC55 · 跳跃游戏 · 首次出现于 Day 40 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC55 · Jump Game · 贪心

跳跃游戏:只留一个数——现在最远能摸到哪

能跳到 i,就可以落到 i 到 i+nums[i] 的任意一格。更远的覆盖已经包含更近的落点,不必给每一格存「能不能到」。

维护变量 farthest = 已扫描位置能到达的最远下标。遍历每个位置 i:若 i > farthest 说明到不了这里,直接返回 false;否则更新 farthest = max(farthest, i+nums[i])。若遍历完成,说明能到终点,返回 true。O(n)。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

给你一个非负整数数组 nums。你最初在下标 0。每个元素 nums[i] 表示在下标 i 最多能向前跳的长度,可以少跳,不能不跳到自己够不着的地方。判断能不能到达最后一个下标。问的是能不能到,不是最少跳几次。

能到的主例 nums = [2, 3, 1, 1, 4] 返回 true:从下标 0 跳 1 步到 1,再跳 3 步到末尾 4。不能到的对照例 nums = [3, 2, 1, 0, 4] 返回 false:怎么跳都会停在下标 3 的 0 上,跨不过去。

中间出现 0 不一定死,只有「最远可达还摸不到这个 0 后面」才死。本文要回答:为什么只记一个右端点就够,以及两段数字并排走时,差的那一格是怎么出现的。

最远覆盖已经包含所有更近的落点

没有这套想法时,人会给每个下标存一个布尔:dp[i] 表示下标 i 能不能到达。dp[0]=true,对每个能到达的 i,把 i+1 到 i+nums[i] 全部标成 true。[2,3,1,1,4] 这样写并不错,但下标 2、3 被涂成真,对「末尾能不能到」没有新信息——只要最远已经盖到 4,中间那些格子必然都能落。每个 i 都可能把一段区间重复涂色,最坏平方。

另一种暴力是在 i 枚举跳 1…nums[i] 每一种落地。数组一长,分支按跳跃宽度爆炸,大量路径其实都落在「当前最远已经覆盖」的区域里。贪心要丢掉的,正是这些被最远覆盖已经蕴含的中间状态。

从「能到下标 0」出发,只问一个更窄的问题:现在手里最远能摸到哪一格?记这个右端点为 farthest。扫描到下标 i 时分两种情况。i > farthest:连 i 都站不到,更后面不用看,失败。i ≤ farthest:i 能站,于是从这里最远落到 i+nums[i],用它去推右端点。

为什么只保留最远不会漏掉一条能成功的路线?因为题目允许少跳。下标 0 写着 2,你可以落到 1,也可以落到 2,不能落到 3——这一步结束后可达集合是 {0,1,2},最远是 2。下标 1 写着 3,并且 1 已经在集合里,于是从 1 可以落到 2、3、4。新的最远是 4,更近的 2、3 不是新发现,是被 4 这块覆盖顺带吃进去的。若最远可达是 F,则 0 到 F 没有空洞。

先走能到的 [2, 3, 1, 1, 4]。farthest 一开始是 0。i=0,站得住,0+2=2。i=1,1≤2,1+3=4,已经盖住末尾。后面 i=2、3、4 都站得住,但覆盖不再往右长。成功路线可以是 0→1→4,也可以是 0→2→3→4。算法没有指定「必须从 1 起跳」,它只证明末尾落在覆盖里。有人把贪心听成「每步选跳得最远的那一格」,会在下标 0 满跳到 2,再抱怨从 2 只能到 3——那是在挑一条具体路线,不是在维护覆盖。

再走不能到的 [3, 2, 1, 0, 4]。i=0,0+3=3。i=1,1+2=3,推不动。i=2,2+1=3。i=3,3+0=3,0 跳不动,最远刚好摸到它。i=4 时 4>3,站不到,失败。卡死点是下标 3 的 0:覆盖钉死在 3,下标 4 的 4 再大也没用,因为人到不了那里。

两段数字并排看:能到的例子里,下标 1 的 3 把覆盖从 2 一下子推到 4;不能到的例子里,前四个数把覆盖钉死在 3,永远差一格。差的不是「有没有 4 这个数字」,而是 4 落在下标 4,人站不到那里。演示场景前三帧走能到,后两帧走到不能到的那一格空洞。

贪心每一步只保留目前看来最有用的那一个边界:最远下标。更远的覆盖包含更近的可达集合,所以丢掉中间那些布尔值不会漏判。找零钱不能当主例——硬币面额一换,局部「先用最大面额」会错。这里能贪,是因为覆盖关系单调。
每一步更新 farthest,够不着就退出
2
0
3
1
1
2
1
3
4
4
farthest = 2farthest=2

六行 Go:贪心最远距离

solution.goGo
func canJump(nums []int) bool {
farthest := 0
for i, n := range nums {
if i > farthest { return false }
if i+n > farthest { farthest = i + n }
}
return true
}

1farthest 是目前可达区间的右端,不是「下一步打算跳到哪」。一开始是 0,人站在下标 0。

2i > farthest:覆盖出现空洞。[3,2,1,0,4] 走到 i=4 时 4>3,走这条 false。

3i+n 是从这格能贡献的新右端。[2,3,1,1,4] 在 i=1 时把 farthest 从 2 推到 4。

4循环能走完,说明每个下标都站得住,末尾可达。能到的例子不必等走完也可以提前收工,逻辑同一句。

总结

只记最远下标。[2,3,1,1,4] 在下标 1 盖到 4;[3,2,1,0,4] 钉死在 3,站不到最后的 4。

  • nums[i] 是最多跳多远,可以少跳。最远覆盖包含所有更近的落点,不必给每一格存布尔。
  • i > farthest 是唯一的失败信号。对照例卡在下标 3 的 0:摸得到它,它却推不动右端点。
  • 不要理解成「每步满跳 nums[i]」。满跳是一条具体路线,覆盖是所有合法落点的上界。
同族题目
LC45跳跃游戏 II(最少步数)LC1306跳跃游戏 IIILC55跳跃游戏