本文将用于分析博主的算法刷题记录,同时也是为了勉励自己每天刷题保持手感,记录博客
这次要介绍的是两道力扣经典贪心算法题
一, 跳跃游戏
附上题目链接

1.思路
题目很简单,让我们找出能否到达楼梯的最顶部
本题我们就使用贪心来解决,关键点在于维护一个可以到达的最远跳跃视野maxLen.
maxLen是变化的, 每走到一个位置,它都会赋予我们一个新的"最远跳跃视野"。
当我们位于i处的台阶上, 台阶上表示的数字代表我们可以跳跃的最远距离,随着我们的跳跃,可能会出现一个问题,
- 当我们所处位置 i < maxLen小 表示这个位置是可以到达的,i + numsi处,
- 当所处位置 i >= maxLen ,说明当前的脚下这步根本走不到(被中间的
0卡死了,无法跳跃),直接返回false。
贪心选择 :我们每次都更新 maxReach = Math.max(maxReach, i + nums[i]),始终保持手里的视野是最远的。
如果能把n遍历完,代表可以走完楼梯,返回true
2. 代码
java
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
int []dp = new int [n+1];
int k = 0;//记录能从起点跳跃到的最远距离
for(int i = 0;i < n;i++){
//i > k说明永远无法跳跃到末尾,maxReach 是单调不减的后续i只会大于k,不可达
if(i > k) return false;
k = Math.max(k,i + nums[i]);
}
return true;
}
}
二,跳跃游戏(二)
附上题目链接

1.思路
本题和上一题的不同点在于是求出到达楼梯顶的最小跳跃次数
对于我们的每次跳跃操作,都会到达一个最终点, 在这个最终点范围内的任何楼梯都能作为起跳点,我们的目的就是在这个范围内找出一个起跳点,让我们能够从这个位置跳跃到达的距离比上一步的最远边界更大
在范围内这个起跳点的存在是未知的,可能永远也无法超出我们之前能够到达的最远距离,所以还需要使用贪心来维护一个变量end作为可达最远距离 , maxReach则代表在end范围内寻找一个能跳的更远的跳板
这就需要我们使用贪心始终更新maxReach
2.代码(贪心)
java
class Solution {
public int jump(int[] nums) {
int jumps = 0; // 跳跃次数
int end = 0; // 当前跳跃步数能达到的最远边界
int nextMaxReach = 0; // 在当前边界内,下一跳能达到的最远边界
// 注意:i < nums.length - 1,不需要遍历最后一个终点
for (int i = 0; i < nums.length - 1; i++) {
// 贪心:在当前能走的范围内,找出能跳得最远的下一个"跳板"
nextMaxReach = Math.max(nextMaxReach, i + nums[i]);
// 走到了当前这一步的边界,必须强制"跳"一下,并切换到新的边界
if (i == end) {
jumps++;
end = nextMaxReach; // 这一步跳完,最远能到新的边界
}
}
return jumps;
}
}
除此之外对于该题还有一种思路则是使用动态规划, 毕竟是我最初想到的解法,虽然时间复杂度为O(N^2)
我们可以定义一个dp表, dpi则表示到达i位置所需要的最小跳跃次数
初始状态, dp0 = 0 ,无需跳跃操作
当我们到达i位置,此时跳跃范围就变为 i , i + nums\[i ] ,我们可以从最远距离i + numsi (姑且称为end吧) 从end向前寻找一个跳板,这个跳板有两个要求
- 可以到达,或者超越end
- 必须距离end越远越好,这代表需要跳跃的次数会更少
所以我们就可以来在向前寻找最远跳板的过程中来维护dp表, 注意这里dpi表示:到达i位置所需要的最小跳跃次数
在最初我们把dp表中除了0 位置的所有值初始化为一个非常大的数,表示初始不可到达
代码(动态规划)
java
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int []dp = new int[n];
//dp[i]:到达i位置所需要的最小跳跃次数
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){//j位置可以通过一次跳跃到达i位置,或者比i更远的位置
//更新最小值
dp[i] = Math.min(dp[j] + 1,dp[i]);
}
}
}
return dp[n-1];
}
}
虽然上述 DP 代码能在 LeetCode 上通过,但 O(n2)O(n^2)O(n2) 的时间复杂度在数据量极大时效率较低。如果我们仔细观察这个 DP 数组,会发现一个非常神奇的数学性质:dpdpdp 数组是单调非递减的。
以 nums = [2, 3, 1, 1, 4] 为例:
-
dp0=0dp0 = 0dp0=0
-
从索引 0 最远到索引 2,所以 dp1,dp2dp1, dp2dp1,dp2 都可以由 dp0+1dp0 + 1dp0+1 得到,即
[0, 1, 1] -
接下来从索引 1 扩散,最远到索引 4,所以 dp3,dp4dp3, dp4dp3,dp4 都可以由 dp1+1dp1 + 1dp1+1 得到,即
[0, 1, 1, 2, 2]
会发现,dpdpdp 数组的值变成了成块的:[0, 1, 1, 2, 2]。
既然它是单调的,当我们在内层循环中从左往右找第一个能到达 iii 的 jjj 时,第一个找到的 jjj 对应的 dpjdpjdpj 一定是最小的!
所以我们根本不需要内层循环去遍历所有的 jjj,只需要用一个指针固定在左边,随着 iii 的右移而缓缓右移。这进一步将时间优化到了 O(n)O(n)O(n),其本质逻辑就与维护"当前步数边界"的贪心算法不谋而合了。
这种用 DP 先推导出正确性,再通过"单调性"优化掉一层循环的思路,在解决各种高阶算法题时是非常强大的通用技巧。