这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
题目
45. 跳跃游戏 II
难度中等
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
代码1 - 回溯
首先,根据题目我们知道:可以按照已知能走的最大台阶数m,我们从0到m,一步一步地去走。
- 直到走到最后一阶,此时不再需要往下走了,那么就返回0.
收集我们上述的结果,容易得知:下一步的结果,只跟下一步上的结果有关。
那么假设我们这次在第i阶走了n台阶,那么:
F(i) = 1 + F(i+n)
根据这个公式我们就可以推出以下的代码来解决这个问题:
private static final int OVERVALUE = 10000000;
public static int jump(int[] nums) {
return jump(nums,0);
}
public static int jump(int[] nums,int curStage){
if(curStage==nums.length-1){
return 0;
}
if(curStage>nums.length-1){
return OVERVALUE;
}
int e = nums[curStage];
int min = OVERVALUE;
for (int i = 1; i <= e; i++) {
min = Math.min(min,1+jump(nums,curStage+i));
}
return min;
}
结果:超时
代码2 - 原始DP
超时至少说明了我们的思路是正确的,接下来我们来优化代码。
首先我们知道:
- 第i阶的最终结果,和之前的状态无关,并且是一个固定的值。
那么实际上,在第i阶的结果,我们可以在计算后记录下来,那么当下一次我们计算第i阶的结果时,我们直接从数组中取就可以,不需要再次去递归计算了。
private static final int OVERVALUE = 10000000;
public static int jump(int[] nums) {
int[] dp = new int[nums.length];
return jump(nums,0,dp);
}
public static int jump(int[] nums,int curStage,int[] dp){
if(curStage==nums.length-1){
return 0;
}
if(curStage>nums.length-1){
return OVERVALUE;
}
if(dp[curStage]!=0){
return dp[curStage];
}
int e = nums[curStage];
int min = OVERVALUE;
for (int i = 1; i <= e; i++) {
min = Math.min(min,1+jump(nums,curStage+i,dp));
}
dp[curStage] = min;
return min;
}
执行用时: 109 ms
内存消耗: 40.1 MB
更进一步 - 顺序DP
为了消除迭代所造成的影响,我们使用顺序的DP,这样子的好处是省去了方法栈的存储,同时我们是顺序往下执行的,没有跳入某一情况再退出的情形。
public static int jump(int[] nums) {
int[] dp = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
int cur = nums[i];
for (int i1 = 1; i1 <= cur; i1++) {
if(i1+i>=dp.length)break;
if( dp[i1+i]!=0) {
dp[i1 + i] = Math.min(dp[i1+i],1+dp[i]);
}else{
dp[i1+i] = 1+dp[i];
}
}
}
return dp[nums.length-1];
}
执行用时:51 ms, 在所有 Java 提交中击败了15.13%的用户
内存消耗:38.9 MB, 在所有 Java 提交中击败了67.30%的用户
显然改进是有效果的,还有没有改进的空间呢?
DP精简 - 贪心
我们可以换一个思路:
-
实际上,我们在迈出一步时,是如何判断我们最后取哪个数的?
- 我们是取当前这一步能走出最远的块作为落脚。
示例:
2 3 1 2 4 2 3
当我们从idx = 0 的地方开始时,我们实际上能走到的是[3,1]:
2 3 1 2 4 2 3
当我们走出第一步的时候(先不管我们选择哪个格子落脚),我们第二步可以到哪一步?
2 3 1 2 4 2 3
那么我们第三步可以走到哪里?
2 3 1 2 4 2 3
显然,我们最少走3步就可以走完了。
那么,我们只要记录我们目前能走到哪一步以及步数就可以了,用代码描述如下:
public static int jump(int[] nums) {
int length = nums.length;
int end = 0;//记录我们当前步数,可以覆盖到的最后一个格子
int maxPosition = 0;//记录我们当前遍历的格子里,最远可以走到哪里
int steps = 0;//记录我们走了多少步
for (int i = 0; i < length - 1; i++) {
maxPosition = Math.max(maxPosition, i + nums[i]);
if (i == end) {
//走到end之后,我们用最大能覆盖的盖上去就可以了
end = maxPosition;
steps++;
}
}
return steps;
}
执行用时:1 ms, 在所有 Java 提交中击败了84.20%的用户
内存消耗:39.4 MB, 在所有 Java 提交中击败了12.22%的用户