1. 程式人生 > >LeetCode打家劫舍問題(動態規劃演算法的理解)

LeetCode打家劫舍問題(動態規劃演算法的理解)

前幾天在LeetCode的時候碰到幾道打家劫舍問題,覺得挺有意思,在這裡跟大家一起學習一下。首先我們來看第一題:
在這裡插入圖片描述

相信有的朋友拿到這道題想法是和我一樣的,那就是暴力解法,我用兩層for迴圈來遍歷每一個房間,後來發現這樣其實太麻煩了,並且還無法考慮到一些特定的情況,所以提交了很多次都沒有提交過。後來看到評論裡有朋友提示使用動態規劃,於是我試著用遞迴的方法去解決這樣的問題,下面詳見程式碼:

class Solution {
    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];
    }
}

首先我們考慮陣列長度小於2的一些情況,這些情況的程式碼都是比較容易理解的,然後後面就是陣列大於2的情況了,這就像斐波那契數列一樣,我們首先定義一個數組初始化數列的前兩位:

int[] result = new int[nums.length];   
result[0] = nums[0];
result[1] = Math.max(nums[0], nums[1]);

這個陣列result表示在第i個房間可以偷得的最高金額,因為不能夠偷連續的兩個房間,所以我們在遞迴的時候,可以表示成:

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

這樣的動態規劃,一層層地遞迴,就可以滿足我們的所有情況,也不用我們考慮種種我們考慮不到的情況了。這樣一來,看似複雜的問題,遞迴拆分之後就變得簡單了。接著這道題又有一道這道題的升級版:
在這裡插入圖片描述

這道題的思路其實和上一題大同小異,只是把首位相連,我們可以將這道題拆分一下,既然首尾相連,我們就把這個陣列拆分成nums[0]-nums[n-1]和nums[1]-nums[n]兩個部分,然後按照上一題的方法分別計算一下兩個陣列的最大金額,然後取這兩個值中較大的那個值就是我們能在這個環形的小區所能夠偷得最大值,最開始拿到這個題目其實看似無從下手,但是如果能想到吧環形的陣列拆分成兩個陣列,就可以將這個問題拆分成上一題的問題,我們來看看程式碼吧:

class Solution {
    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[] result1 = new int[nums.length - 1];
        int[] result2 = new int[nums.length - 1];
        
        result1[0] = nums[0];
        result1[1] = Math.max(nums[0], nums[1]);
        
        result2[0] = nums[1];
        result2[1] = Math.max(nums[1], nums[2]);
        
        for(int i = 2; i < result1.length; i++){
            result1[i] = Math.max(nums[i] + result1[i - 2], result1[i - 1]);
        }
        
        for(int j = 2; j < result2.length; j++){
            result2[j] = Math.max(nums[j + 1] + result2[j - 2], result2[j - 1]);
        }
        return Math.max(result1[nums.length - 2], result2[nums.length - 2]);
    }
}

這裡需要注意的是,因為我們將陣列進行了拆分,所以在數組裡面的引數不要弄錯了,這題的思想和上一題其實是一樣的,關鍵是能否考慮到將環形的小區拆分成兩個線性的小區。 現在還有個打家劫舍的升級題目,就是將我們的陣列換成了二叉樹:
在這裡插入圖片描述

在拿到這個二叉樹的題目的時候,發現毫無思路可言,但是想到動態規劃,我們去找一下這個二叉樹小區的規律,我們可以發現,在二叉樹中,如果偷了一個節點,那麼和它相鄰的節點都不可以偷了,也就是說,偷了二叉樹中的某一層的話,那麼和這一層相鄰的層都不可以偷了,我們將房間的概念換成二叉樹層的概念就方便理解了,我們同樣可以用動態規劃的思想:

class Solution {
    public int rob(TreeNode root) {
        if(root == null)
            return 0;
        int s0 = 0;
        int s1 = root.val;        
        s0 = rob(root.left) + rob(root.right);     
        if(root.left != null)
            s1 += rob(root.left.left) + rob(root.left.right);
        if(root.right != null)
            s1 += rob(root.right.left) + rob(root.right.right);
        return Math.max(s0,s1);
    }
}

s1表示偷根節點所在的那一層和不和它相鄰的層,s0表示偷和根節點相鄰的那一層,然後我們用動態規劃的思想就可以解決這個問題了。其實這一類問題的解法並不難,但是很難想到動態規劃如何用在這類似的題目當中,希望這篇博文能夠讓大家對動態規劃演算法的理解有所提升,謝謝。