1. 程式人生 > >LeetCode--55. Jump Game

LeetCode--55. Jump Game

題目連結:https://leetcode.com/problems/jump-game/

這是個十分有趣的題目。從起點(陣列索引0)出發,移動k(k<=nums[0])個位置到達下一個位置(陣列索引為i),繼續移動k(k<=nums[i])個位置,就這樣看是否有一條走法能到達終點(陣列索引nums.length-1)。當走到某個位置i時nums[i]==0時就意味著這個走法行不通了!可以看到理論上每一個位置i上都有nums[i]個選擇的步數。

我的思路:回溯法來暴力搜尋,在當前到達的位置狀態i上,有1:nums[i]種選擇(只要nums[i]+i<nums.length),然後就進入下一個(位置)狀態繼續搜尋。

class Solution {
    public static boolean canJump(int[] nums) {
        return recursion(0,nums);
    }
    
    public static boolean recursion(int index,int[] nums)
    {
        if(index==nums.length-1)
            return true;
        if(nums[index]==0)
            return false;
        boolean ret=false;
        for(int i=1;i<=nums[index];i++)
        	if(index+i<nums.length)
        		ret=ret || recursion(index+i,nums);
           
        return ret;
    }
}

時間複雜度:O(2^n),從起點到達終點有2^n的走法(理論上限)

空間複雜度:O(n),遞迴時的棧記憶體開銷

暴力是真暴力,就是TLE了!!!

其實審查上面的過程,到達位置狀態i的策略選擇可以由很多很多種,而後面從i為起點的搜尋只需要進行一次,然而我的演算法存在大量重複。

看了Solutions後豁然開朗,https://leetcode.com/problems/jump-game/solution/這篇題解將回溯、動態規劃講的很全面很詳細了,下面借鑑這篇題解寫寫自己的理解

上面的步驟在實踐中可以稍微優化一下,從較大調步開始搜尋更快到達終點,但演算法理論複雜度沒變。

for(int i=nums[index];i>0;i--)

自頂向下的動態規劃:

用一個memo陣列儲存位置i對於終點的可達性(accesibility),終點對於其本身是可達的,首先將memo陣列memo[0:nums.length-2]=Unknown;

enum Mark{
    Accessible,Inaccessible,Unknown
}

public class Solution {
    
    public Mark[] memo;
    
    public boolean canJump(int[] nums) {
        
        memo=new Mark[nums.length];
        
        for(int i=0;i<memo.length;i++)
            memo[i]=Mark.Unknown;
        memo[nums.length-1]=Mark.Accessible;
        return canJumpFromIndex(0,nums);
    }
    
    
    public boolean canJumpFromIndex(int index,int[] nums){
        
        if(memo[index]!=Mark.Unknown)
            return memo[index]==Mark.Accessible? true:false;
        
        int min_index=Math.min(nums[index]+index,nums.length-1);
        for(int i=min_index;i>=index+1;i--)
        {
            if(canJumpFromIndex(i,nums))
            {
                memo[index]=Mark.Accessible;
                return true;
            }
        }
        memo[index]=Mark.Inaccessible;
        return false;   
    }
}

時間複雜度為:O(n^2),因為對於每個狀態位置i,都有可能將[i+1:nums.length-1]遍歷一遍

空間複雜度:O(n),遞迴的棧空間開銷O(n),額外陣列開銷O(n)

 

自底向上的動態規劃:自底向上一般能夠將遞迴改成迴圈的形式,而且能夠減少中間重複的計算

確定第i個位置的可達性只需要檢查第[i+1,i+min(nums[i]+i,nums.length-1)]的位置上的可達性,真是tricky!!!

public class Solution {
    
    public Mark[] memo;
    
    public boolean canJump(int[] nums) {
        
        memo=new Mark[nums.length];
        
        for(int i=0;i<memo.length;i++)
            memo[i]=Mark.Unknown;
        memo[nums.length-1]=Mark.Accessible;
        for(int i=nums.length-2;i>=0;i--)
        {
            int max_index=Math.min(nums[i]+i,nums.length-1);
            for(int j=max_index;j>i;j--)
            {
                if(memo[j]==Mark.Accessible)
                {
                    memo[i]=Mark.Accessible;
                    break;
                }
            }
        }
        return memo[0]==Mark.Accessible?true:false;
    }
}

enum Mark{
    Accessible,Inaccessible,Unknown
}