【中等】45. 跳跃游戏 II

0 阅读3分钟

给定一个长度为 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 <= 104
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 n - 1

1. 生活案例:公交接力挑战

想象你要去一个很远的地方(终点),但你身上没钱,只能靠刷脸坐免费公交。

  • 规则

    • 每一站的公交车都有不同的最大行驶距离(数组中的数字)。
    • 你可以选择在当前车能到的任何一站下车,换乘那一站的另一辆车。
  • 你的目标:用最少的换乘次数到达终点。

  • 你的贪心策略

    1. 你坐在第一辆车上,眼睛盯着窗外所有你能下车的站点。
    2. 你心里在盘算: “如果我在 A 站下,下一辆车最远能带我到哪?如果在 B 站下,下一辆又最远能到哪?”
    3. 你并不急着下车,直到第一辆车快没油了(到达了当前航程的最远边界),你才决定: “好,就在刚才看好的那一站换乘,因为那一站的接力车能跑得最远!”

2. 代码解析与“生活化”注释

代码的核心在于实时维护一个“当前能达到的最远范围”,并在必须换乘时才增加步数。

JavaScript

/**
 * @param {number[]} nums - 每一站公交车能跑的最大距离
 * @return {number} - 到达终点的最少换乘次数(步数)
 */
var jump = function(nums) {
    let n = nums.length;
    // 如果只有一站,原地就是终点,跳 0 次
    if (n === 1) return 0;

    let steps = 0;    // 跳跃次数(换乘次数)
    let farthest = 0; // 坐在当前车上时,眼光所及能达到的“最远接力点”
    let end = 0;      // 当前这这一跳能跑到的“边界限制”

    // 注意:我们不需要遍历最后一项,因为到达最后一项前,
    // 我们已经通过前面的“接力”确保能跳过或恰好落在那了。
    for (let i = 0; i < n - 1; i++) {
        // 生活化解释:
        // 坐在车上,每经过一站 i,都看看这一站的接力车能跑多远 (i + nums[i])
        // 永远记录下那个最给力的“接力候选人”
        farthest = Math.max(farthest, i + nums[i]);

        // 当我走到了当前这辆车的“油尽灯枯”点(边界)
        if (i === end) {
            // 必须换乘了!
            steps = steps + 1;
            // 换乘到刚才看好的那个最给力的候选车上,
            // 现在的最远边界就变成了那个候选车能跑到的地方
            end = farthest;
        }
    }

    return steps;
};

3. 为什么代码这样写?(算法本质)

  1. 不是动态规划:虽然这题可以用 DP 做,但复杂度是 O(n2)O(n^2)。用贪心可以降到 O(n)O(n)
  2. 贪心的智慧:我们不是在每一站都跳,而是在不得不跳的时候(到达 end),才选择之前路上看到的潜力最大的那一站进行“虚拟换乘”。
  3. 局部最优 -> 全局最优:每一次换乘都确保下一次的覆盖范围最大化,从而保证了总跳跃次数最少。

总结

这道题的关键在于区分 farthest(眼光)end(脚步)

  • 你的眼光(farthest)一直在搜寻最有潜力的下一跳。
  • 你的脚步(end)只有在没路可走时才真正迈出一步,并瞬间跟上眼光的进度。