1. 程式人生 > >leetcode 198 打家劫舍 (一維動態規劃、基礎)

leetcode 198 打家劫舍 (一維動態規劃、基礎)

題目:

你是一個職業盜賊,鎖定了一條大街準備今晚作案,街上每棟房子裡都有固定量的財務。但是相鄰的房子之間有報警器,一旦兩個相鄰的房子在同一晚被盜,就會觸發報警器。現已知一系列非負整數表示每個房子裡的財務數目,請計算在不觸發報警器的情況下,你今晚可以盜取的最大值。

思路:

因為我可以選擇偷或者不偷,所以我們會有兩個分條件。假設共有四個房子ABCD。那麼我們會有以下兩個分選擇:


於是我們可以知道,雖然我們開始想要得到在ABCD中進偷竊的最大收益,但是現在這個問題變成了兩個子問題:

  • 計算在CD中的最大收益,然後加上A房子的收益

  • 計算在BCD中的最大收益

然後,我們比較以上兩個選項,就能知道在ABCD中選擇的最大收益。

針對在BCD中選擇房子,獲得最大收益的問題,我們可以同樣的進行拆解。


這時候,我們發現 計算在CD中選擇的最大收益。這個問題在ABCD的時候也出現了。我們可以重新計算一遍,但是也可以把之前的答案儲存下來,這樣就不需要計算了。

那麼我們需要儲存的答案都有哪些呢?ABCD、BCD、CD、D。也就是從一個節點到終點的情況。於是我們可以構建陣列memo[x],其中的x表示的是剩餘元素的個數。

於是上述的四種情況,分別對應memo[4]、memo[3]、memo[2]、memo[1]。

因此,我們的目標是memo[4],它的結果可以拆解為兩個子問題:

  • memo[2] + Value_A

  • memo[3]

同理,當我們計算memo[3]的時候,它的結果可以拆解為兩個子問題:

  • memo[2] + Value_B

  • memo[1]

 然後選出這兩種情況的最大值,遞迴執行,直到index<0。

    public int solve(int index, int[] nums){
        if(index < 0){
            return 0;
        }
        int max = Math.max(nums[index] + solve(index - 2, nums), solve(index - 1, nums));
        return max;
    }

    public int rob(int[] nums) {
        return
solve(nums.length-1, nums); }
  • 這裡寫圖片描述

此種暴力方法在執行第56個測試用例時,超出時間限制。 
假設我們搶n-1家,那麼接下來的執行方案: 
n-1 ->(n-3, n-4, n-5) 
假設我們搶n-2家,那麼接下來的方案為: 
n-2 ->(n-4, n-5) 
那麼我的兩種決策方式只是影響能不能搶n-3,在n-3之後都是隨便搶的;通過觀察上述兩種方案,我們發現了n-4,n-5被重複計算。因此,每一家都有兩種可能,搶或者不搶。則該演算法的時間複雜度為:O(2n)。 

為了避免上述的重複計算(優化為動態規劃方法),我們初始化一個數組來記錄此下標是否被計算過,將陣列初始化為-1,如果當前index被算過,就記錄下來。

如果這個index被計算過,那麼我們直接返回這個index對應的值即可,這就是去冗餘,採用空間換時間的方法。因此當n-1房屋的最優解算過後,就能推匯出n房屋的最優解。這就是動態規劃的思想。 

因此,我們考慮使用動態規劃,設定result[]陣列記錄搶奪該房屋可能的最大收益,同時用來記錄此房屋是否被搶過。 
自頂向下解法
class Solution {
    public static int[] result;

    public int solve(int index, int[] nums){
        if(index < 0){
            return 0;
        }

        if(result[index] >= 0){
            return result[index];
        }

        result[index] = Math.max(nums[index] + solve(index-2 , nums), solve(index-1, nums));
        return result[index];
    }

    public int rob(int[] nums) {
        result = new int[nums.length];
        for(int i=0; i < result.length; i++){
            result[i]=-1;
        }
        return solve(nums.length-1, nums);
    }
}

對於每個房屋我們都算了一次,那麼時間複雜度為O(n)

自底向上解法

    public int rob(int[] nums) {
        if (nums.length == 0){
            return 0;
        }

        if (nums.length == 1){
            return nums[0];
        }

        if (nums.length == 2){
            return Math.max(nums[0], nums[1]);
        }

        int[] result = new int[nums.length];   
        result[0] = nums[0];//只有一家的情況下,只能搶這一家了
        result[1] = Math.max(nums[0], nums[1]);//有兩家可選的情況下,搶價值最大的那一家

        for(int index=2; index < result.length; index++){
            result[index] = Math.max(nums[index] + result[index-2], result[index -1]);
        }

        return result[nums.length -1];
    }
}

自底向上,編碼簡單,遞推(自頂向下為遞迴)。既然自底向上不需要遞迴,那麼就不需要solve函數了。我們只要處理好邊界條件,然後計算即可。

C++程式碼:

非常簡潔:

使用dp[i]表示到第i個房間時得到的最大金額數,得到狀態轉移方程:

dp[i]=max{dp[i-1],dp[i-2]+money[i]};

class Solution {  
public:  
    int rob(vector<int>& nums) {  
        if(nums.size()==0) return 0;  
        int i,dp[100000];  
        dp[0]=nums[0];  //必須先初始化前兩個值
        dp[1]=nums[0]>nums[1]?nums[0]:nums[1];  
        for(i=2;i<nums.size();i++){  
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);  
        }  
        return dp[nums.size()-1];  
    }  
};  

通常利用動態規劃的共性:

本質:遞迴