【leetCode】 - 跳跃游戏二

439 阅读3分钟

这是我参与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%的用户