题目描述:
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[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 <= 1040 <= nums[i] <= 1000- 题目保证可以到达
nums[n-1]
思路:
考虑贪心的思路。每次在能跳到的范围内,找到能跳到最远的位置,作为下一跳的候选。例如,当前步骤能到达的最远位置是curEnd,而在这段区间内的每个位置i,可以到达的最远位置是i + nums[i]。我们需要在这些可能的位置中,找到最大的那个,作为下一步的最远点,并增加跳跃次数。这可能是一种方法。
举个例子,比如初始的时候,我们在位置0,可以跳1或2步。这时候,第一个区间是0到0,因为初始位置是0。然后在这个区间内,计算每个位置能到达的最远距离。比如位置0能到达的最远是0+2=2。这时候,下一步的最远位置是2,所以跳跃次数加1。接下来,下一个区间是1到2的位置,也就是第1和第2个元素。在这两个位置里,计算能到达的最远位置。比如位置1可以跳到4(1+3=4),而位置2只能跳到3(2+1=3)。所以下一跳的最远位置是4,此时跳跃次数加1。这时候,已经到达或超过最后一个位置(索引4),所以结束,总共有2次跳跃。
这个思路可能有效。我们需要维护几个变量:当前能到达的最远位置max,当前能跳到的最远位置end,以及跳跃的次数step。
实现:
class Solution {
public int jump(int[] nums) {
int max = 0;
int end = 0;
int step = 0;
int n = nums.length;
if (n==1) return 0;
for (int i = 0; i < n; i++) {
max = Math.max(max, nums[i]+i);
if (i == end) {
end = max;
step++;
if (max >= n-1) break;
}
}
return step;
}
}
这种算法被称为贪心算法,是因为它在每一步都做出当前最优的选择,通过局部最优解的积累来达到全局最优。以下是具体解释:
1. 贪心算法的核心特征
贪心算法的核心是:在每一步选择中,只考虑当前状态下能获得的最好结果,而不考虑后续步骤的潜在影响。虽然这听起来可能短视,但某些问题(如本题)的数学性质可以保证这种策略能得到全局最优解。
2. 跳跃游戏 II 的贪心策略
在本题中,我们的目标是 以最少跳跃次数到达终点。具体实现中的贪心策略是:
- 当前能跳多远?
遍历数组时,维护一个 当前能跳到的最远位置(cur_farthest)。 - 何时必须跳跃?
当遍历到 当前跳跃的边界(cur_end)时,必须做一次跳跃,并更新边界为cur_farthest。 - 为什么这是最优选择?
每次跳跃时,选择 能覆盖最远未来可能性的位置(即尽可能跳得远),这样可以减少后续需要的跳跃次数。
3. 为什么贪心是有效的?
• 数学性质保证正确性:
由于问题具有 贪心选择性质(每次选最远跳跃不会破坏全局最优解),且 最优子结构(全局最优解包含局部最优解),因此贪心策略有效。
• 直观理解:
跳跃得越远,后续需要跳跃的次数越少。例如:
示例:nums = [2, 3, 1, 1, 4]
第一次跳跃选择从位置 0 跳到位置 1(覆盖更远),而不是跳到位置 2。
虽然位置 2 更近,但位置 1 的下一跳能覆盖到终点。
4. 对比其他可能的策略
如果不用贪心,可能需要暴力枚举所有可能的跳跃路径(时间复杂度爆炸)。而贪心算法通过 每一步的局部最优选择,避免了穷举,将时间复杂度降为 O(n)。
5. 贪心的关键点
• cur_farthest:记录所有已遍历位置中能跳到的最远位置。
• cur_end:当前跳跃的边界,到达此处时必须跳跃。
• jumps:跳跃次数,每次到达 cur_end 时递增。
6. 总结
贪心算法的本质是:在问题的每一步,做出当前看来最好的选择,并相信这些选择最终能导向全局最优解。在跳跃游戏 II 中,通过选择能覆盖最远范围的跳跃,确保了跳跃次数的最小化。