动态规划 跳跃游戏 II

LeetCode 45. 跳跃游戏 II

问题描述

给定一个非负整数数组 nums,初始位于数组的第一个位置。数组中的每个元素代表在该位置可以跳跃的最大长度。目标是使用最少的跳跃次数到达数组的最后一个位置(假设总是可以到达)。

示例

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
     
示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

方法一:动态规划(反向查找)

算法思路

  1. 状态定义dp[i] 表示从位置 i 到达数组末尾的最小跳跃次数
  2. 初始化:末尾位置不需要跳跃(dp[n-1] = 0)。
  3. 状态转移:对于每个位置 i,遍历它能跳到的所有位置 ji+1i+nums[i]),取 dp[j] + 1 的最小值。
  4. 结果:返回 dp[0](从起点到终点的最小跳跃次数)。

代码实现

class Solution {
    public int jump(int[] nums) {
        int n = nums.length;
        // dp[i] 表示从位置 i 到达末尾的最小跳跃次数
        int[] dp = new int[n];
        // 初始化:最后一个位置不需要跳跃
        dp[n - 1] = 0;
        
        // 从后向前计算每个位置的最小跳跃次数
        for (int i = n - 2; i >= 0; i--) {
            // 如果当前位置可以直接跳到末尾
            if (i + nums[i] >= n - 1) {
                dp[i] = 1;
            } else {
                // 初始化为一个较大值(表示暂时不可达)
                dp[i] = Integer.MAX_VALUE - 1; // 防止后续 +1 溢出
                // 遍历当前位置能跳到的所有位置
                for (int j = i + 1; j <= i + nums[i] && j < n; j++) {
                    // 如果位置 j 可达,则更新 dp[i]
                    if (dp[j] != Integer.MAX_VALUE) {
                        dp[i] = Math.min(dp[i], dp[j] + 1);
                    }
                }
            }
        }
        return dp[0];
    }
}

复杂度分析

  • 时间复杂度:O(n²),其中 n 是数组长度。每个位置可能需要遍历最多 n 个位置。
  • 空间复杂度:O(n),用于存储 dp 数组。

方法二:贪心算法(正向查找)

算法思路

  1. 核心变量
    • steps:记录当前跳跃次数。
    • end:当前步数能到达的最远边界。
    • maxPos:下一步能到达的最远位置。
  2. 遍历数组
    • 更新 maxPos = max(maxPos, i + nums[i])
    • 当遍历到当前边界 end 时:
      • 跳跃次数 steps++
      • 更新边界 end = maxPos(表示进入下一步)。
    • 提前终止:如果 end 已覆盖终点,直接结束。
  3. 结果:返回跳跃次数 steps

代码实现

class Solution {
    public int jump(int[] nums) {
        int n = nums.length;
        int steps = 0;      // 跳跃次数
        int end = 0;        // 当前步数能到达的边界
        int maxPos = 0;     // 下一步能到达的最远位置
        
        // 遍历数组(注意终点不需要再跳)
        for (int i = 0; i < n - 1; i++) {
            // 更新下一步能跳到的最远位置
            maxPos = Math.max(maxPos, i + nums[i]);
            // 到达当前步数的边界,必须进行下一次跳跃
            if (i == end) {
                steps++;       // 增加跳跃次数
                end = maxPos;  // 更新边界
                // 如果新边界已覆盖终点,提前结束
                if (end >= n - 1) break;
            }
        }
        return steps;
    }
}

复杂度分析

  • 时间复杂度:O(n),只需遍历数组一次。
  • 空间复杂度:O(1),仅使用常数空间。

算法过程(贪心法)

nums = [2,3,1,1,4]

  1. 初始化steps=0, end=0, maxPos=0
  2. 遍历
    • i=0maxPos = max(0, 0+2)=2 → 到达边界 end=0steps=1, end=2
    • i=1maxPos = max(2, 1+3)=4(边界未更新)。
    • i=2maxPos = max(4, 2+1)=4 → 到达边界 end=2steps=2, end=4
    • i=3maxPos 不变 → 边界 end=4 已覆盖终点,循环结束。
  3. 结果steps=2

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1: 标准示例
    int[] nums1 = {2,3,1,1,4};
    System.out.println("Test 1: " + solution.jump(nums1)); // 2
    
    // 测试用例2: 起点即终点
    int[] nums2 = {0};
    System.out.println("Test 2: " + solution.jump(nums2)); // 0
    
    // 测试用例3: 需要连续跳跃
    int[] nums3 = {1,2,1,1,1};
    System.out.println("Test 3: " + solution.jump(nums3)); // 3
    
    // 测试用例4: 大跨度跳跃
    int[] nums4 = {2,3,0,1,4};
    System.out.println("Test 4: " + solution.jump(nums4)); // 2
    
    // 测试用例5: 递增序列
    int[] nums5 = {1,2,3,4,5};
    System.out.println("Test 5: " + solution.jump(nums5)); // 3
}

关键点

  1. 动态规划
    • 反向计算最小跳跃次数。
    • 适用于理解状态转移,但效率较低(O(n²))。
  2. 贪心算法
    • 核心思想:每一步都跳到未来潜力最大的位置。
    • 边界更新:当遍历到当前边界时,意味着必须进行下一次跳跃。
    • 提前终止:若边界已覆盖终点,直接结束。
    • 最优选择:时间复杂度 O(n),空间复杂度 O(1),是本题最优解法。

常见问题

  1. 为什么贪心算法中循环条件是 i < n-1
    终点不需要跳跃,且当 end 覆盖终点时可提前终止。
  2. 贪心算法的正确性保证
    每一步都扩展了下一步的最大覆盖范围,确保跳跃次数最小。
  3. 动态规划为何效率低
    每个位置需遍历其后所有可达位置,最坏情况时间复杂度 O(n²)。

为什么动态规划使用反向查找

1. 问题依赖关系的方向性

  • 跳跃方向:从位置 i 只能向后跳跃(i → i+1, i+2, ..., i+nums[i])。
  • 状态依赖:要计算 dp[i](从 i 到终点的最小步数),必须知道 i 后方位置的状态(dp[i+1], dp[i+2], …)。
  • 反向计算:从终点开始倒序计算,确保计算 dp[i] 时,其依赖的所有 dp[j]j > i)都已计算完成。

2. 正向查找的缺陷

若尝试正向计算(从起点向终点):

public int jump(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = 0;
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (j + nums[j] >= i) {
                dp[i] = Math.min(dp[i], dp[j] + 1);
            }
        }
    }
    return dp[n-1];
}
  • 问题 1:计算 dp[i] 时,dp[j] 可能未达到最优解(动态规划要求无后效性)。
  • 问题 2:需要遍历所有 j < i,最坏时间复杂度 O(n²),且无法利用跳跃特性优化。

3. 反向查找的优势

(1) 自然符合状态依赖
for (int i = n - 2; i >= 0; i--) {
    for (int j = i + 1; j <= i + nums[i]; j++) {
        dp[i] = Math.min(dp[i], dp[j] + 1); // 依赖后方状态
    }
}
  • 倒序计算确保 dp[j]dp[i] 之前已求解。
(2) 提前终止优化
if (i + nums[i] >= n - 1) {
    dp[i] = 1; // 直接跳到终点
}
  • 若从 i 可直接跳到终点,无需遍历后续位置。
(3) 逻辑清晰
  • 状态定义dp[i] = "从 i 到终点的最小步数" 符合直觉。
  • 目标明确:直接返回 dp[0](起点到终点的解)。

无后效性要求

动态规划的核心要求之一是无后效性:一个子问题的解一旦确定,就不应受后续状态的影响:

  1. 状态定义应该包含确定问题解的所有必要信息
  2. 状态转移不应依赖未求解的状态
  3. 状态一旦计算完成,其值不应再改变
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值