【LeetCode难题解题思路(Java版)】45. 跳跃游戏 II

问题:

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:

输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
说明:

假设你总是可以到达数组的最后一个位置。

输入输出:

class Solution {
    public int jump(int[] nums) {
    }
    }

方案设计:

采用递归的方式,寻找从第i个数开始,到达最后所需的最少的步数,很简单,直接贴代码。

class Solution {
    public int jump(int[] nums) {
    	return jum(nums,0);
    }
    public int jum(int[] nums,int i){
        if(i==nums.length-1){
            return 0;
        }
        if(i+nums[i]>=nums.length-1){
            return 1;
        }
        int value=nums.length;//步数最大也就是n个数,走n步
        for(int j=1;j<=nums[i];j++){
            if(i+j<nums.length-1&&nums[i+j]>0){
                value=Math.min(value,1+jum(nums,i+j));
            } 
        }
        return value;
    }
}

提交的结果是:71 / 92 个通过测试用例。在下面这个测试用例里报出超时:

[5,6,4,4,6,9,4,4,7,4,4,8,2,6,8,1,5,9,6,5,2,7,9,7,9,6,9,4,1,6,8,8,4,4,2,0,3,8,5]

很明显,递归的写法太简单,时间复杂度太高,才到了71个用例就卡死了,不行,要优化。这里开始采用递归向动态规划转化的思想。学习了《算法导论》里的动态规划的一章,以及要特别感谢一下这篇博客:
算法-动态规划 Dynamic Programming–从菜鸟到老鸟

优化一:

一般来说,递归耗时太久是因为重复的计算了太多的单元,比如一共有200个数字要计算,其中第188个数字(计算从它开始至少要几次才能跳到最后),可能要计算几十次,这样肯定是不科学的,所以,递归向动态规划转换的第一步,可以先考虑下备忘录的思想,用空间换时间,将已经计算过的结果保存一下,下一次直接用就可以了。代码如下:

class Solution {
    public int jump(int[] nums) {
        int[] v=new int[nums.length];//用它保存从第i位开始,往后至少要几步
        for(int i=0;i<v.length;i++){
            v[i]=-1;
        }
        return jum(nums,0,v);
    }
    public int jum(int[] nums,int i,int[] v){
        if(v[i]>-1){//如果已经有值,则直接返回
            return v[i];
        }
        if(i==nums.length-1){
            v[i]=0;
            return 0;
        }
        if(i+nums[i]>=nums.length-1){
            v[i]=1;
            return 1;
        }
        int value=Integer.MAX_VALUE-1;
        for(int j=1;j<=nums[i];j++){
            if(i+j<nums.length-1&&nums[i+j]>0){
                value=Math.min(value,1+jum(nums,i+j,v));
            }
            
        }
        v[i]=value;
        return value;
        
    }
}

运行的结果是:91 / 92 个通过测试用例,只有最后一个报了超时,最后一个的测试用例非常长,有兴趣的可以点进去看一下,测试用例。这个测试用例我单独运行了一下,直接就报了超时,时间都没法统计。所以,说明备忘录的方式是有效果的,但是还远远不够,所以还要接着优化,那么就进行动态规划,自底向上的编程思想。

优化二:

之前的设计是,寻找从第i位开始向后至少需要几位,这种思想是递归的思想是自顶向下的,用于动态规划自底向上就不是很适合。所以要转换下思想,设一个数组v[],v[i]保存的不再是从第i位开始,往后至少需要几步,而是保存到达i时,至少需要几步,这样的话,i从0开始往后递推,就能得到最后的结果,而且是每次递推,都依赖上一次的结果,正好是自底向上的思想。言语说可能有点不太好理解,画图说明:
初始状态
把v数组的第0位设为0很容易理解,其后的我设为了5,因为数据一共5个,最大的步数也不会超过5,也比较容易理解。然后依次处理各个数据,处理的方式是,首先得到nums[i]的值,它代表了能往后跳多远,而v[i]代表了从开始到第i位,至少要跳多少步,所以,要刷新v[j],i+1<=j<=i+nums[i],判断如果是v[i]+1(从当前位往后跳一次)比v[i+j]小的话,就令v[i+j]=v[i]+1,这样就得到了v[i+j]的值(到达第i+j至少需要几步)。比如处理第一个数据的时候,过程如图:
处理i=0
然后依次是:
接下来的处理
最后就得到了结果。
代码如下:

class Solution {
    public int jump(int[] nums) {
        int length=nums.length;
        int[] v=new int[length];//定义v表示到达数组的当前位置至少需要几步
        for(int i=1;i<length;i++){
            v[i]=length;
        }
        for(int i=0;i<length;i++){//第一个不用考虑,肯定是0
            for(int j=1;j<=nums[i]&&i+j<length;j++){
                if(v[i]+1<v[i+j]){
                    v[i+j]=v[i]+1;
                }
            }
        }
        return v[nums.length-1];
    }
}

执行这个代码,结果还是超时,依旧是最后一个用例没有通过,我单独运行了一下这个用例,发现时间已经有了,是317ms。
单独运行结果
说明思路应该是对的,就是里面有些关键的东西被我忽略了,期间我注意到了,其实当第一次到达结尾的时候,就已经得到最终答案了,所以我尝试着进行了这样的改进:

class Solution {
    public int jump(int[] nums) {
        int length=nums.length;
        int[] v=new int[length];//定义v表示到达数组的当前位置至少需要几步
        if(length==1){
            return 0;
        }
        for(int i=1;i<length;i++){
            v[i]=length;
        }
        for(int i=0;i<length;i++){//第一个不用考虑,肯定是0
            if(i+nums[i]>=length-1){//第一次到达则立刻返回
                return v[i]+1;
            }
            for(int j=1;j<=nums[i]&&i+j<length;j++){
                if(v[i]+1<v[i+j]){
                    v[i+j]=v[i]+1;
                }
                
            }
        }
        return v[nums.length-1];
    }
}

最终的结果是,只提高了2ms,还是超时,说明仍需要优化。

优化三

优化二的基础之上,i的递增是每次+1,其实,是没有必要这样的,比如对于数组[4,2,2,2,5,6],从4直接跳到5即可,中间的2的计算,是没有任何意义的,那么i每次+1就要优化成这样:i每次要走最佳一大步,这一大步可以定义成一步能覆盖的范围,范围越大,这一步越优,具体讲,就是从当前位置开始,往后走一步加上走一步后的那个位置最大能覆盖到的举例。比如4,它可以走到,2,2往后能覆盖2位,也就是它最多推进3位,同理,后面的2分别推进4,5位,而5,则能推进9位,也就是说下一步从5开始往后推进就行,之前的不可能比它更优。
代码如下:

class Solution {
    public int jump(int[] nums) {
        int length=nums.length;
        int[] v=new int[length];//定义v表示到达数组的当前位置至少需要几步
        if(length==1){
            return 0;
        }
        for(int i=1;i<length;i++){
            v[i]=length;
        }
        int i=0;
        int max=0;
        int ss=0;
        while(i<length){//第一个不用考虑,肯定是0
            if(i+nums[i]>=length-1){
                return v[i]+1;
            }
            max=nums[i+1]+1;
            ss=1;
            for(int j=1;j<=nums[i]&&i+j<length;j++){
                if(nums[i+j]+j>max){
                    max=nums[i+j]+j;
                    ss=j;
                }
                if(v[i]+1<v[i+j]){
                    v[i+j]=v[i]+1;
                }
                
            }
            i=i+ss;
        }
        return v[nums.length-1];
    }
}

提交结果是12ms,终于通过了~但是只超越了40%的人。而作为一个铁头娃,我依然不满足。

就看了一下评论区,发现还可以再优化。

优化四

其实就这个问题来说,v[i]其实也可以不需要,因为推进的时候,已经将推进优化成了每次寻找宽度最大的下一个数字,那么定义一个step,每寻找一次,step++,最后到达末尾的时候,直接输出不就得了嘛~
代码如下:

class Solution {
    public int jump(int[] nums) {
        int length=nums.length;
        if(length==1){
            return 0;
        }
        int i=0;
        int max=0;
        int ss=0;
        int step=0;
        while(i<length){//第一个不用考虑,肯定是0
            if(i+nums[i]>=length-1){
                return ++step;
            }
            max=nums[i+1]+1;
            ss=1;
            for(int j=1;j<=nums[i]&&i+j<length;j++){
                if(nums[i+j]+j>max){
                    max=nums[i+j]+j;
                    ss=j;
                }  
            }
            i=i+ss;//优化了一下递进的速度
            step++;
        }
        return step;
    }
}

运行结果是9ms或者10ms,超越72%。作为一个铁头娃,不超越90%我是不会满足的。于是我又看了一下评论区。

优化五

直接贴那个老哥的代码:

class Solution {
    public int jump(int[] nums) {
       if(nums.length == 1) return 0;
        int reach = 0;
        int nextreach = nums[0];//第一步能走多远,初始化
        int step = 0;
        for(int i = 0;i<nums.length;i++){
            nextreach = Math.max(i+nums[i],nextreach);//一步能走多远
            if(nextreach >= nums.length-1) return (step+1);
            if(i == reach){//记步,
                step++;
                reach = nextreach;
            }
        }
        return step;
    }
}

实际上思想是一样的,就是换了种写法,少了点儿步骤,但是这种写法在一些需要多次循环的运算里确实是有很大作用的,所以程序的优化,思想是第一步,然后程序的精简和科学性是第二步,当然,这个版本的可读性是最差的,因为就像高数老师给讲题一样,不说前面直接看这个,基本上是不可能看懂的。运行一下,结果是7ms,超越92%。
一本满足。最后再看下程序,发现一个问题,这特么不是贪心算法吗。。。
在这里插入图片描述
看来还是需要继续培养自己的解决问题的直觉呀。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章