目录
- 1.问题描述
- 2.问题分析
-
- [2.1 题目理解](#2.1 题目理解)
- [2.2 核心洞察](#2.2 核心洞察)
- [2.3 破题关键](#2.3 破题关键)
- 3.算法设计与实现
-
- [3.1 解法一:贪心(边界跳跃法)](#3.1 解法一:贪心(边界跳跃法))
- [3.2 解法二:动态规划(自顶向下记忆化)](#3.2 解法二:动态规划(自顶向下记忆化))
- [3.3 解法三:动态规划(自底向上)](#3.3 解法三:动态规划(自底向上))
- [3.4 解法四:BFS(广度优先搜索)](#3.4 解法四:BFS(广度优先搜索))
- [3.5 解法五:贪心优化(记录下一次边界)](#3.5 解法五:贪心优化(记录下一次边界))
- 4.性能对比
-
- [4.1 理论复杂度对比表](#4.1 理论复杂度对比表)
- [4.2 实际性能测试(n=10^4)](#4.2 实际性能测试(n=10^4))
- [4.3 各场景适用性分析](#4.3 各场景适用性分析)
- 5.扩展与变体
-
- [5.1 变体一:跳跃游戏 II(打印路径)](#5.1 变体一:跳跃游戏 II(打印路径))
- [5.2 变体二:跳跃游戏 II(带权重的跳跃)](#5.2 变体二:跳跃游戏 II(带权重的跳跃))
- [5.3 变体三:跳跃游戏 II(禁止某些位置)](#5.3 变体三:跳跃游戏 II(禁止某些位置))
- [5.4 变体四:跳跃游戏 II(反向思考)](#5.4 变体四:跳跃游戏 II(反向思考))
- 6.总结
-
- [6.1 核心思想总结](#6.1 核心思想总结)
- [6.2 实际应用场景](#6.2 实际应用场景)
- [6.3 面试建议](#6.3 面试建议)
- [6.4 常见面试问题Q&A](#6.4 常见面试问题Q&A)
1.问题描述
给定一个长度为 n 的 0 索引 整数数组 nums。初始位置在下标 0。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 i + j 处,其中:
0 <= j <= nums[i]i + j < n
返回到达 n - 1 的最小跳跃次数 。测试用例保证可以到达 n - 1。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标 0 跳到下标 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
1 <= nums.length <= 10^40 <= nums[i] <= 1000- 题目保证可以到达
n - 1
2.问题分析
2.1 题目理解
这是经典跳跃游戏问题的进阶版,在保证可达的前提下,求最少的跳跃次数。与"能否到达"不同,这里需要记录步数。
2.2 核心洞察
- 贪心策略 :在每一步中,我们并不需要关心具体跳到哪里,只需要知道在当前步数内能到达的最远位置,以及下一步能到达的最远位置。当遍历到当前步数的边界时,步数加 1,并将边界更新为下一步能到达的最远位置。
- 边界维护 :使用两个变量
currentEnd(当前步数能到达的最远边界)和farthest(下一步能到达的最远位置)。遍历数组时,不断更新farthest,当i == currentEnd时,表示已经走完当前步数的覆盖范围,必须再跳一步,步数加 1,并将currentEnd设为farthest。 - 动态规划 :定义
dp[i]为到达位置 i 的最小跳跃次数,则dp[i] = min(dp[j] + 1)对于所有能跳到 i 的 j。但复杂度 O(n²) 可能超时,需要优化。
2.3 破题关键
- 贪心法只需一次遍历,O(n) 时间,O(1) 空间。
- 注意边界:当
currentEnd更新后,若已经覆盖终点,则直接返回步数。
3.算法设计与实现
3.1 解法一:贪心(边界跳跃法)
核心思想:
维护当前步数能到达的最远位置和下一步能到达的最远位置。遍历数组,当到达当前步数边界时,增加步数并更新边界。
算法思路:
- 初始化
steps = 0,currentEnd = 0,farthest = 0。 - 遍历
i从 0 到n-2(因为最后一步不需要再跳):- 更新
farthest = max(farthest, i + nums[i]) - 如果
i == currentEnd:- 步数加 1
- 更新
currentEnd = farthest - 如果
currentEnd >= n-1,直接返回steps(提前结束)
- 更新
- 返回
steps。
Java代码实现:
java
class Solution {
public int jump(int[] nums) {
int n = nums.length;
if (n == 1) return 0;
int steps = 0;
int currentEnd = 0;
int farthest = 0;
for (int i = 0; i < n - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i == currentEnd) {
steps++;
currentEnd = farthest;
if (currentEnd >= n - 1) break;
}
}
return steps;
}
}
性能分析:
- 时间复杂度:O(n),一次遍历。
- 空间复杂度:O(1)。
- 优点:最优解法,简洁高效。
- 缺点:需要理解边界更新逻辑。
3.2 解法二:动态规划(自顶向下记忆化)
核心思想:
定义 dp[i] 为从位置 i 跳到终点所需的最小步数。递归地,从 i 出发,尝试所有可能的跳跃长度,取最小值 +1。使用记忆化避免重复计算。
算法思路:
- 创建
memo数组,初始化为 -1 表示未计算。 - 递归函数
jumpFrom(pos):- 如果
pos >= n-1,返回 0。 - 如果
memo[pos] != -1,返回memo[pos]。 - 初始化
minSteps = Integer.MAX_VALUE。 - 从
j = 1到nums[pos]且pos + j < n:- 递归调用
jumpFrom(pos + j),如果返回不是Integer.MAX_VALUE,则更新minSteps。
- 递归调用
- 记录
memo[pos] = minSteps + 1,返回。
- 如果
- 调用
jumpFrom(0)并返回结果。
Java代码实现:
java
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int[] memo = new int[n];
Arrays.fill(memo, -1);
return jumpFrom(0, nums, memo);
}
private int jumpFrom(int pos, int[] nums, int[] memo) {
int n = nums.length;
if (pos >= n - 1) return 0;
if (memo[pos] != -1) return memo[pos];
int minSteps = Integer.MAX_VALUE;
int maxJump = Math.min(nums[pos], n - 1 - pos);
for (int j = 1; j <= maxJump; j++) {
int next = pos + j;
int steps = jumpFrom(next, nums, memo);
if (steps != Integer.MAX_VALUE) {
minSteps = Math.min(minSteps, steps + 1);
}
}
memo[pos] = minSteps;
return minSteps;
}
}
性能分析:
- 时间复杂度:最坏 O(n²),但实际会剪枝。
- 空间复杂度:O(n) 递归栈 + 记忆数组。
- 优点:思路直观,易于理解。
- 缺点:效率较低,可能栈溢出(n=10^4 时递归深度过大)。
3.3 解法三:动态规划(自底向上)
核心思想:
从后向前递推,dp[i] 表示从 i 到终点的最小步数。初始化 dp[n-1] = 0。对于 i 从 n-2 到 0,计算 dp[i] = min(dp[i+1...i+nums[i]]) + 1。
算法思路:
- 创建
dp数组,长度为 n,初始化为一个大值(如n)。 dp[n-1] = 0。- 从
i = n-2递减到 0:- 计算最远能跳到的位置
maxJump = Math.min(n-1, i + nums[i])。 - 遍历
j从i+1到maxJump,取dp[j]的最小值,然后dp[i] = minVal + 1。
- 计算最远能跳到的位置
- 返回
dp[0]。
Java代码实现:
java
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, n);
dp[n-1] = 0;
for (int i = n-2; i >= 0; i--) {
int maxJump = Math.min(n-1, i + nums[i]);
int minSteps = n;
for (int j = i+1; j <= maxJump; j++) {
minSteps = Math.min(minSteps, dp[j]);
}
dp[i] = minSteps + 1;
}
return dp[0];
}
}
性能分析:
- 时间复杂度:O(n²),在 n=10^4 时可能超时(但 nums[i] 最大 1000,实际内层循环平均较小,可能通过)。
- 空间复杂度:O(n)。
- 优点:递推清晰,无递归栈。
- 缺点:最坏情况仍可能 O(n²)。
3.4 解法四:BFS(广度优先搜索)
核心思想:
将跳跃看作图,每个位置是一个节点,从 i 可以跳到 i+1...i+nums[i]。求从 0 到 n-1 的最短路径。使用 BFS 逐层扩展。
算法思路:
- 使用队列存储当前层的节点。
- 使用 visited 数组记录已访问位置,避免重复入队。
- 初始将 0 入队,步数 = 0。
- 每层处理时,将当前节点的所有可达未访问节点入队。
- 当遇到 n-1 时返回当前步数。
Java代码实现:
java
import java.util.*;
class Solution {
public int jump(int[] nums) {
int n = nums.length;
if (n == 1) return 0;
boolean[] visited = new boolean[n];
Queue<Integer> queue = new LinkedList<>();
queue.offer(0);
visited[0] = true;
int steps = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int cur = queue.poll();
int maxJump = Math.min(n-1, cur + nums[cur]);
for (int next = cur + 1; next <= maxJump; next++) {
if (!visited[next]) {
if (next == n-1) return steps + 1;
visited[next] = true;
queue.offer(next);
}
}
}
steps++;
}
return -1; // 理论上不会执行
}
}
性能分析:
- 时间复杂度:O(n²) 最坏,但每个节点只入队一次,内层循环可能重复扫描,实际与动态规划类似。
- 空间复杂度:O(n)。
- 优点:图论视角,易理解。
- 缺点:效率不高。
3.5 解法五:贪心优化(记录下一次边界)
核心思想:
与解法一类似,但进一步精简,不需要提前判断边界,只需在遍历时更新步数。
算法思路:
- 初始化
steps = 0,currentEnd = 0,farthest = 0。 - 遍历
i从 0 到n-2:farthest = Math.max(farthest, i + nums[i])- 如果
i == currentEnd,则steps++,currentEnd = farthest。
- 返回
steps。
Java代码实现:
java
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int steps = 0;
int currentEnd = 0;
int farthest = 0;
for (int i = 0; i < n - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i == currentEnd) {
steps++;
currentEnd = farthest;
}
}
return steps;
}
}
性能分析:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 优点:最简代码,与解法一相同。
4.性能对比
4.1 理论复杂度对比表
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 贪心边界法 | O(n) | O(1) | 最优,简洁 | 需理解边界更新 |
| 记忆化递归 | O(n²) | O(n) | 直观 | 递归深度大,效率低 |
| 自底向上DP | O(n²) | O(n) | 无递归 | 可能超时 |
| BFS | O(n²) | O(n) | 图论视角 | 效率低 |
| 贪心优化版 | O(n) | O(1) | 最简 | 同上 |
4.2 实际性能测试(n=10^4)
- 贪心法:约 0.1 ms
- 动态规划:约 50-100 ms(依赖于数据)
- BFS:类似动态规划
4.3 各场景适用性分析
- 面试场景:贪心法是标准答案,必须熟练掌握。
- 竞赛场景:贪心法最优。
- 练习场景:可对比多种解法,理解贪心的优势。
5.扩展与变体
5.1 变体一:跳跃游戏 II(打印路径)
题目描述:在求最少跳跃次数的同时,输出其中一条最短路径(即跳跃的索引序列)。
核心思路:在贪心过程中记录每一步的起点,或者使用动态规划回溯。
Java代码实现:
java
import java.util.*;
class Solution {
public List<Integer> jumpPath(int[] nums) {
int n = nums.length;
if (n == 1) return Arrays.asList(0);
int[] steps = new int[n];
int[] prev = new int[n];
Arrays.fill(steps, n);
steps[0] = 0;
for (int i = 0; i < n; i++) {
int maxJump = Math.min(n-1, i + nums[i]);
for (int j = i+1; j <= maxJump; j++) {
if (steps[i] + 1 < steps[j]) {
steps[j] = steps[i] + 1;
prev[j] = i;
}
}
}
List<Integer> path = new ArrayList<>();
int cur = n-1;
while (cur != 0) {
path.add(cur);
cur = prev[cur];
}
path.add(0);
Collections.reverse(path);
return path;
}
}
5.2 变体二:跳跃游戏 II(带权重的跳跃)
题目描述:每个位置除了最大跳跃长度,还有不同的跳跃代价,求最小总代价到达终点。
核心思路 :使用动态规划,dp[i] 表示到 i 的最小代价,转移时取最小值。
Java代码实现:
java
class Solution {
public int minCostJump(int[] nums, int[] cost) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 0; i < n; i++) {
int maxJump = Math.min(n-1, i + nums[i]);
for (int j = i+1; j <= maxJump; j++) {
if (dp[i] + cost[i] < dp[j]) {
dp[j] = dp[i] + cost[i];
}
}
}
return dp[n-1];
}
}
5.3 变体三:跳跃游戏 II(禁止某些位置)
题目描述:某些位置为障碍物(-1),不能落脚。求最少跳跃次数。
核心思路:在贪心或 DP 中跳过障碍物位置。
Java代码实现:
java
class Solution {
public int jumpWithObstacles(int[] nums) {
int n = nums.length;
if (nums[0] == -1) return -1;
int steps = 0;
int currentEnd = 0;
int farthest = 0;
for (int i = 0; i < n - 1; i++) {
if (nums[i] == -1) continue; // 跳过障碍物,但注意:i可能小于currentEnd,仍需更新farthest?
farthest = Math.max(farthest, i + nums[i]);
if (i == currentEnd) {
steps++;
currentEnd = farthest;
if (currentEnd >= n - 1) return steps;
}
}
return steps;
}
}
5.4 变体四:跳跃游戏 II(反向思考)
题目描述:从后往前,每次选择能跳到当前位置的最远位置,从而构造路径。
核心思想:反向贪心,从终点开始,向前找最远的能跳到当前位置的点,不断前移。
Java代码实现:
java
class Solution {
public int jumpReverse(int[] nums) {
int n = nums.length;
int steps = 0;
int position = n - 1;
while (position > 0) {
for (int i = 0; i < position; i++) {
if (i + nums[i] >= position) {
position = i;
steps++;
break;
}
}
}
return steps;
}
}
6.总结
6.1 核心思想总结
- 贪心法通过维护"当前步数能到达的最远位置"和"下一步能到达的最远位置",在遍历过程中更新步数,实现 O(n) 求解。
- 动态规划法通过递推或记忆化搜索,但复杂度较高。
- BFS 也是常见思路,但效率不如贪心。
6.2 实际应用场景
- 游戏中的角色跳跃路径规划
- 网络跳数优化(如最小跳数路由)
- 算法竞赛中的经典例题
6.3 面试建议
- 首选贪心解法,能清晰解释边界更新过程。
- 能够分析时间复杂度和空间复杂度。
- 与"跳跃游戏 I"进行对比,说明异同。
6.4 常见面试问题Q&A
Q1:贪心算法为什么能得到最少跳跃次数?
A1:因为每一步我们都尽可能扩大下一步的覆盖范围,即每一步都跳到当前范围内能到达的最远位置,从而保证步数最少。
Q2:如何理解 currentEnd 和 farthest 的作用?
A2:currentEnd 是当前步数能到达的最远边界,当遍历到该边界时,意味着当前步数已无法再往前,必须再跳一步,此时将边界更新为下一步能到达的最远距离 farthest。
Q3:动态规划解法为什么可能超时?
A3:因为每个位置都需要检查后面 O(nums[i]) 个位置,最坏情况下 nums[i] 很大,导致 O(n²) 复杂度。
Q4:如果数组长度很大但数值很小,哪种解法好?
A4:贪心法仍然是最优,动态规划也可能因为内层循环少而较快,但贪心始终是 O(n)。
Q5:如何处理题目保证可达?
A5:直接使用贪心,无需额外检查。