45. 跳跃游戏 II - 力扣(LeetCode)
解法1:(动态规划 + 贪心)
果然代码越短,思路越难。这题用的是动态规划+贪心的思想。首先分析题意我们可以知道,从索引0这个点开始,我们走一步可以到达一些点,最远可以到达的点自然就是0 + nums[0]
,所以能从0走一步到达的点是不止一个的,它们组成了一个组,下标范围是1 ~ 0 + nums[0]
,这些下标到达所需的最少步数就是1,这就形成了局部最优,之后我们如法炮制,再去找走两步可以到达哪些点,找之前,我们首先应该明确一下,为什么这样做是对的?会不会存在我们接下来求出的那些最少走两步可以到达的点,它们实际上还有更优的解呢?比如走一步就能到呢?答案是否定的。如果这个点走一步就能到达,我们知道,肯定是从下标0开始走,走一步到达的。但是我们对于下标0求出了一个走一步所能到达的范围,也就是0 + nums[0]
,如果下标范围没在这个里面,那是肯定走一步是没法到达的,其实不单单走一步能到走两步能到这个例子,后面同样也是如此分析的,这也是一种单调性,我们随着下标越来越大,每个下标对应的最少能到达的步数是只会递增的,这点是很关键的,所以我们可以用动态规划的思想,用前一个状态去更新后一个状态,因为我们知道,走两步可以到达的点,肯定是从那些走一步可以到达的点当中某一个点走一步到达的,所以我们就可以用那些走一步可以达到的的点,去更新出走两步可以到达的店(注意,我说的这些修饰词:走一步、走两步啥的,都是相对于起点,也就下标0而言的)。寻找走两步可以到达的点,肯定是从走一步可到达的点组的右边界的下一个点开始,也就是0 + nums[0] + 1
,从这个点开始,我们用双指针思想,第二个指针从走一步的点组的左边界开始枚举,去找走一步的点组中第一个可以走一步到达0 + nums[0] + 1
这个下标的点,然后将这个点的值更新为走两步(也就是2),这里可能会有疑问?为什么非要找走一步的点组中的第一个呢?这个其实也不是,理论上单单对于求0 + nums[0] + 1
这个下标的最少步数,我们是无所谓的,无论从走一步点组中的哪一点走,反正都是走一步可以到这个点,0 + nums[0] + 1
这个下标的最少步数肯定是被更新成2,不管是从哪个点走过来的。但是我们从走一步点组中的点从左边界到右边界枚举,也是有一个目的:确保最优,如果存在更优解会覆盖之前不那么优秀的解。而且枚举每个走一步的点组,因为它们的下标不同嘛,nums[i]
也不同,所以每一个点走一步所能到达最远的点都是不一样的。所以用走一步点组的中的这些点去更新出来的点,也是一个点组,这个点组也就是走两步点组了。并且我们保证了走两步点组中的所有点,它们都是最少的步数,它们之中都不存在,明明一步能到的,我们给他更新成了两步(这个之前也分析了),同样也不存在的是,这个走两步点组的右边界的下一个点一定是需要走三步才能到的,也就是说我们把走两步点组的下标范围,也是更新成了最大的范围,不能再大了。所以这样就保证了我们解的最优性。这也是贪心的思想,每一个当前最优的状态的,是从上一个最优的状态更新过来的,上一个是最优状态,再走一步到达的这个点,肯定也是最优状态,这也就是这题动态规划+贪心的魅力所在吧。
当然,我这是文字通俗解释,会特别冗长多余,需要简洁解释的小伙伴可以参考:
LeetCode 45. 跳跃游戏 II - AcWing
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> f(n);
for(int i = 1, j = 0;i < n; i ++ ) {
while( j + nums[j] < i) j ++ ;
f[i] = f[j] + 1;
}
return f[n -1];
}
};
解法2 : (贪心 + 双指针)
class Solution {
public:
int jump(vector<int>& nums) {
//贪心 + 双指针
int n = nums.size();
//维护三个指针,cur表示当前位置,dis表示目前可到达的最远位置,next表示每次枚举途中可以到达的最远位置
int ans = 0, cur = 0, dis = 0;
while(dis < n - 1) {
//next是每一轮枚举的临时变量,定义在里面的
int next = 0;
while(cur <= dis) {
next = max(next,cur + nums[cur]);
cur ++;
}
ans ++;
dis = next;
}
return ans;
}
};