LeetCode经典算法面试题 #45:跳跃游戏II(贪心法、动态规划、BFS等多种实现方案详解)

目录

  • 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.问题描述

给定一个长度为 n0 索引 整数数组 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^4
  • 0 <= 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 解法一:贪心(边界跳跃法)

核心思想

维护当前步数能到达的最远位置和下一步能到达的最远位置。遍历数组,当到达当前步数边界时,增加步数并更新边界。

算法思路

  1. 初始化 steps = 0currentEnd = 0farthest = 0
  2. 遍历 i 从 0 到 n-2(因为最后一步不需要再跳):
    • 更新 farthest = max(farthest, i + nums[i])
    • 如果 i == currentEnd
      • 步数加 1
      • 更新 currentEnd = farthest
      • 如果 currentEnd >= n-1,直接返回 steps(提前结束)
  3. 返回 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。使用记忆化避免重复计算。

算法思路

  1. 创建 memo 数组,初始化为 -1 表示未计算。
  2. 递归函数 jumpFrom(pos)
    • 如果 pos >= n-1,返回 0。
    • 如果 memo[pos] != -1,返回 memo[pos]
    • 初始化 minSteps = Integer.MAX_VALUE
    • j = 1nums[pos]pos + j < n
      • 递归调用 jumpFrom(pos + j),如果返回不是 Integer.MAX_VALUE,则更新 minSteps
    • 记录 memo[pos] = minSteps + 1,返回。
  3. 调用 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

算法思路

  1. 创建 dp 数组,长度为 n,初始化为一个大值(如 n)。
  2. dp[n-1] = 0
  3. i = n-2 递减到 0:
    • 计算最远能跳到的位置 maxJump = Math.min(n-1, i + nums[i])
    • 遍历 ji+1maxJump,取 dp[j] 的最小值,然后 dp[i] = minVal + 1
  4. 返回 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 逐层扩展。

算法思路

  1. 使用队列存储当前层的节点。
  2. 使用 visited 数组记录已访问位置,避免重复入队。
  3. 初始将 0 入队,步数 = 0。
  4. 每层处理时,将当前节点的所有可达未访问节点入队。
  5. 当遇到 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 解法五:贪心优化(记录下一次边界)

核心思想

与解法一类似,但进一步精简,不需要提前判断边界,只需在遍历时更新步数。

算法思路

  1. 初始化 steps = 0currentEnd = 0farthest = 0
  2. 遍历 i 从 0 到 n-2
    • farthest = Math.max(farthest, i + nums[i])
    • 如果 i == currentEnd,则 steps++currentEnd = farthest
  3. 返回 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:如何理解 currentEndfarthest 的作用?

A2:currentEnd 是当前步数能到达的最远边界,当遍历到该边界时,意味着当前步数已无法再往前,必须再跳一步,此时将边界更新为下一步能到达的最远距离 farthest

Q3:动态规划解法为什么可能超时?

A3:因为每个位置都需要检查后面 O(nums[i]) 个位置,最坏情况下 nums[i] 很大,导致 O(n²) 复杂度。

Q4:如果数组长度很大但数值很小,哪种解法好?

A4:贪心法仍然是最优,动态规划也可能因为内层循环少而较快,但贪心始终是 O(n)。

Q5:如何处理题目保证可达?

A5:直接使用贪心,无需额外检查。

相关推荐
黎阳之光3 小时前
黎阳之光:数智硬核技术赋能应急管理装备创新,筑牢安全防线
大数据·人工智能·科技·算法·安全
进击的小头3 小时前
第19篇:卡尔曼滤波器与MPC模型预测控制器的结合实战
python·算法
2501_908329853 小时前
C++中的装饰器模式
开发语言·c++·算法
2301_788770553 小时前
OJ模拟2
数据结构·算法
Q741_1473 小时前
每日一题 力扣 3548. 等和矩阵分割 II 前缀和 哈希表 C++ 题解
算法·leetcode·前缀和·矩阵·力扣·哈希表
木井巳3 小时前
【递归算法】全排列 Ⅱ
java·算法·leetcode·决策树·深度优先·剪枝
Fcy6483 小时前
算法竞赛有关数据结构的补充(3)—— 二叉树、堆和哈希表的静态实现(包括红黑树和AVL树动态实现)
数据结构·算法·散列表
代码探秘者3 小时前
【算法篇】6.分治
java·数据结构·后端·python·算法·排序算法