题目介绍
力扣45题:https://leetcode-cn.com/problems/jump-game-ii/
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。
分析
本题跳跃规则跟上题一致,并且已经保证可以到达最后一个位置,现在是希望得到最小的跳跃步数。要想步数最小,很容易想到的一个思路是,每一步都迈到最大,也就是直接跳到当前能达到的最远位置。这是典型的贪心策略。
但仔细分析就会发现问题:我们每次从i跳到最远farthest,相当于跳过了中间i到farthest之间的那些元素,接下来就只能按farthest位置的步数前进了。如果farthest位置的元素很小(甚至可能为0),而中间被跳过的元素很大,那我们之前的选择明显出现了偏差。每一步的状态会影响到后续的选择,并不是“无后效”的,所以不能用这样简单的贪心策略。
方法一:反向跳跃
们可以尝试用逆向思维来思考,也就是反向跳跃。
现在我们就不是从第一个位置出发了,而是从最后一个位置出发逆推。我们首先可以得到,哪些位置,可以一步直接跳到最后。而为了让步数最少,我们可以选择让最后一次跳跃最远,也就是说,最后一跳之前所在的位置,距离最后最远。这实际上,也是一种贪心策略。
找到最后一步跳跃前所在的位置之后,我们继续贪心地寻找倒数第二步跳跃前所在的位置,以此类推,直到找到数组的开始位置。
代码如下:
// 方法一:反向跳跃
public int jump1(int[] nums){
// 定义一个变量保存跳跃步数
int steps = 0;
// 定义循环变量
int curPosition = nums.length - 1;
// 不停地向前跳跃,以最远的距离
while (curPosition > 0){
// 从前到后遍历数组元素,找到当前距离最远的“上一步位置”
for (int i = 0; i < curPosition; i++){
if (i + nums[i] >= curPosition){
curPosition = i; // 从前到后,第一次能跳到当前位置的位置,就是最远的上一步位置
steps ++;
break;
}
}
}
return steps;
}
复杂度分析
- 时间复杂度:O(n^2),其中 n 是数组长度。有两层嵌套循环,在最坏的情况下,数组中的所有元素都是1,那么要寻找的“上一步”有n个,而每次寻找都需要遍历数组中的每个位置。
- 空间复杂度:O(1)。
方法二:正向跳跃
我们可以回忆起之前的跳跃游戏中的算法,每次都贪心地找到当前位置i最远能到达的位置farthest,但不能直接跳过去,而是依次遍历接下来的每个位置,继续更新farthest。
原因就在于,i与farthest之间的元素,可能在下一步跳得很远。综合两步来看,我们并不能确定当前i这一步跳到最远,两步之后也跳得最远。
那自然可以想到,我们也考虑长远一点,正向推导的时候考虑两步,就可以保证最优了。这还是一个贪心策略。
那这里有一个问题,第一步选了(下标)0 -> 1,怎么保证如果选0 -> 2之后不会超过去呢?
我们可以看到,第二步,是肯定超不过的,因为我们第一步选下标1,就是因为它第二步跳得远;第三步,如果基于第一步选0 -> 2,想要跳得更远,就一定会有第二步跳到的位置x能跳非常远。我们可以发现,由于x不会超过0 -> 1之后最远的第二步,所以如果可以0 -> 2 -> x,那就一定能0 -> 1 -> x。所以第一步选位置1一定没有问题。
具体实现,我们可以定义双指针:一个farthest,指向当前这一步跳跃的极限位置,它就是以当前位置为基准、跳跃一步的最远位置;另一个nextFarthest,指向下一次跳跃的极限位置,它应该是遍历直到farthest的所有元素,得到基于当前位置跳跃两步的最远位置。遍历数组,每次到达farthest的时候,跳跃次数加1,并把极限位置更新为当前能到达的最远位置。
代码如下:
// 方法二:正向跳跃,考虑能够跳到最远的两步
public int jump(int[] nums){
int steps = 0;
// 定义双指针,指向当前位置跳一步和两步分别能到的最远位置
int farthest = 0;
int nextFarthest = farthest;
// 不停贪心寻找下一步的合适位置
// while (farthest < nums.length - 1){
// 遍历currPosition~farthest范围内所有元素,选择第二步跳跃最远的作为当前一步的选择
for (int i = 0; i < nums.length - 1; i++){
// 如果比之前第二步最远距离大,更新
if (i + nums[i] > nextFarthest){
nextFarthest = i + nums[i];
}
// 添加步数增长条件:处理到farthest
if (i == farthest){
// 当前一步完成
steps ++;
farthest = nextFarthest;
}
}
// }
return steps;
}
复杂度分析
- 时间复杂度:O(n),其中 n 是数组长度。
- 空间复杂度:O(1)。