【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%。
一本滿足。最後再看下程序,發現一個問題,這特麼不是貪心算法嗎。。。
在這裏插入圖片描述
看來還是需要繼續培養自己的解決問題的直覺呀。。。

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