leetcode - [動態規劃] - 打家劫舍

1、問題描述

假設你是一個小偷,計劃盜竊沿街的房屋,每個房屋都藏有一定數量的現金,影響你盜竊的唯一制約是每兩間相鄰的房屋裝有相互連通的報警裝置,如果你盜竊了兩間相鄰的房屋就會觸發報警裝置。
給定一個非負整數數組表示每個房屋藏有的現金金額。計算在不觸動報警裝置的情況下,能盜竊到的最大金額。
例子1:

輸入: [1,2,3,1]
輸出: 4
解釋: 偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。

例子2:

輸入: [2,7,9,3,1]
輸出: 12
解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。
偷竊到的最高金額 = 2 + 9 + 1 = 12 。

2、解題思路

方法1:暴力法。最簡單的方法是窮舉所有的盜竊方案,然後找到對應的最大盜取金額。
問題的解空間樹如下所示(以例1爲例說明):
在這裏插入圖片描述
只要使用深度優先搜索這顆樹即可,僞代碼如下所示:

#dfs(current, len, curcash, maxcash, nums):
	*if(current + 2 < len):
		#maxcash = max(curcash,maxcash)
		#return
	*for(next = current + 2; next < len; next++):
		#curcash += nums[next]
		#dfs(next, len, curcash, maxcash, nums)
		#curcash -= nums[next]

注意,這種方法的時間複雜度並不是指數級的,因爲解空間樹的分支數量爲(n2)+(n3)+...+1=(n1)(n2)2(n-2)+(n-3)+...+1=\frac{(n-1)(n-2)}{2},因此時間複雜度O(n2)O(n^2),空間複雜度爲遞歸的深度,爲O(n)O(n)
方法2:動態規劃1. 從上面的解空間樹可以看出,存在最優子結構,例如,盜竊的最後一個房屋爲4號房屋所能獲得最大金額等於盜竊的最後一個房屋爲2號房屋所能獲得最大金額+4號房屋的金額,與盜竊的最後一個房屋爲1號房屋所能獲得的最大金額+4號房屋的金額,這兩者之間的最大值。
根據這一個發現,我們採用與LIS相同的動態規劃思路來解決這個問題:
(1)定義狀態
dp[i]dp[i]:盜竊的最後一個房屋爲第ii號房子所能獲得的最大金額;
(2)狀態轉移
dp[i]=max{dp[i],dp[j]+nums[i]},j[0,i2]dp[i] =max\{dp[i],dp[j] + nums[i] \},j\in[0,i-2]
盜竊的最後一個房屋爲第ii號房所能獲得的最大金額等於以前i2i-2個房子中的第jj號房屋能獲得的金額的最大值加上第ii號房屋所藏的金額。
(3)確定初始
dp[i]=nums[i],i<2dp[i] = nums[i], i<2
(4)確定終止
max{dp[i]},i[0,len)max\{dp[i]\}, i\in[0,len)
時間複雜度爲O(n2)O(n^2),空間複雜度O(n)O(n)
方法3:動態規劃2。從另一個角度來看這個問題,假設你是小偷,對於第ii家,你有兩種選擇,打劫或不打劫(0、1問題)。

  1. 如果你要打劫第ii家,那麼你必然不能打劫第i1i-1家,所以獲得的錢就等於打劫前i2i-2家的得到的錢加上第ii家的錢;
  2. 如果你不打劫第ii家,那麼打劫得到的錢就等於打劫前i1i-1得到的錢。

所以打劫第ii家獲得的最大金額應該是這兩種選擇中獲得的最優解。
(1)定義狀態
dp[i]dp[i]:打劫前ii家所能獲得的金額;
(2)狀態轉移
dp[i]=max{dp[i1],dp[i2]+nums[i]}dp[i] = max\{dp[i-1],dp[i-2]+nums[i]\}
(3)確定初始
dp[0]=nums[0]dp[0] = nums[0]
dp[1]=max{nums[0],nums[1]}dp[1] = max\{nums[0],nums[1]\}
(4)確定終止
dp[len1]dp[len-1]

優化:由於求每一步只用到了前兩個最大值,因此只要兩個變量就夠了。

3、代碼實現

// class Solution {
// public:
//     int rob(vector<int>& nums) {
//         int len = nums.size();
//         vector<int> maxcash(len,0);
//         for(int i = 0; i < len; ++i){
//             if(i < 2){
//                 maxcash[i] = nums[i];
//                 continue;
//             }
//             for(int j = i - 2; j >=0; --j){
//                 maxcash[i] = max(maxcash[i], maxcash[j] + nums[i]);
//             }

//         }
//         int ans = 0;
//         for(int k = 0; k < len; ++k){
//             ans = max(ans,maxcash[k]);
//         }
//         return ans;

//     }
// };
class Solution{
    public:
    int rob(vector<int>& nums){
        int len = nums.size();
        if(len == 0){
            return 0;
        }
        if(len == 1){
            return nums[0];
        }
        if(len == 2){
            return max(nums[0],nums[1]);
        }
        
        int lasttwo = nums[0];
        int lastone = max(nums[0], nums[1]);
        for(int i = 2; i < len; ++i){
            int temp = lastone;
            lastone = max(lastone, lasttwo + nums[i]);
            lasttwo = temp;
            
        }
        return lastone;
    }
};

4、拓展問題

4.1 拓展問題1

問題:打家劫舍II
(1)如果房屋圍成一個圓圈,即表示每個房屋所藏金錢的非負整數數組爲環形數組,計算小偷所能盜竊的最大金額。
示例1:

輸入: [1,2,3,1]
輸出: 4
解釋: 你可以先偷竊 1 號房屋(金額 = 1),然後偷竊 3 號房屋(金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4

分析:圍成一個圈,這意味着偷了第一家就不能偷最後一家,即只能選擇在第1~至len -1家盜竊或第0至len - 2家盜竊。最後能獲得的最大金額爲這兩者中的最大值。

class Solution {
public:
    int rob(vector<int>& nums) {
        int len = nums.size();
        if(len == 0){
            return 0;
        }
        if(len == 1){
            return nums[0];
        }
        //盜竊1~len-1號房子所能獲得的最大金額;
        int gain1 = robhelper(nums,1,len - 1);
        // cout<<"gain1="<<gain1<<endl;
        //盜竊0~len-2號房子所能獲得的最大金額;
        int gain2 = robhelper(nums, 0, len - 2);
        // cout<<"gain2="<<gain2<<endl;

        return max(gain1, gain2);
        

    }
    
    int robhelper(vector<int>& nums, int start, int end){
        int len = end - start + 1;
        if(len == 1){
            return nums[start];
        }
        if(len == 2){
            return max(nums[start], nums[start + 1]);
        }

        int lasttwo = nums[start];
        int lastone = max(nums[start], nums[start + 1]);
        for(int i = start + 2; i <= end; ++i){
            int temp = lastone;
            lastone = max(lastone, lasttwo + nums[i]);
            lasttwo = temp;
        }
        return lastone;
    }
};

4.2 拓展問題2

問題:打家劫舍III
(2)這個擴展問題是將房屋組織成一個二叉樹,限制同樣是如果在同一天打劫了相鄰的兩間房屋,就會觸發報警裝置,計算在不觸發報警裝置的情況下,能盜竊到的最大金額。
示例1:

輸入: [3,2,3,null,3,null,1]

  3
 / \    
2   3
\    \ 
 3    1

輸出: 7 解釋: 小偷一晚能夠盜取的最高金額 = 3 + 3 + 1 = 7.

示例 2:

輸入: [3,4,5,1,3,null,1]

    3
   / \    
  4   5   
 / \   \   
1   3   1

輸出: 9 解釋: 小偷一晚能夠盜取的最高金額 = 4 + 5 = 9.

分析:對於每個節點rootroot(每一間房屋),有兩種選擇:

  1. 如果選擇偷節點rootroot,那麼它的孩子節點就不能偷;
  2. 如果選擇不偷節點rootroot,那麼對於節點rootroot所能盜竊的最大金額就等於它的左、右孩子所能盜竊的最大金額之和。
    根據以上分析,動態規劃的具體過程如下:
    (1)定義狀態
    dp[root][status]dp[root][status]:節點rootroot選擇狀態爲stautsstauts時所能盜竊的最大金額,statusstatus有兩種0、1兩種取值,0表示偷,1表示不偷;
    (2)狀態轉移
    dp[root][0]=max{dp[root.left][0],dp[root.left][1]}+max{dp[root.right][0],dp[root.right][1]}dp[root][0] =max\{dp[root.left][0],dp[root.left][1]\}+max\{dp[root.right][0],dp[root.right][1]\}
    dp[root][1]=dp[root.left][0]+dp[root.right][0]+root.valdp[root][1] =dp[root.left][0] + dp[root.right][0]+root.val
    當選擇不偷節點ii時,能獲得的最大盜竊金額爲爲左孩子能獲得的最大盜竊金額+右孩子所能獲得最大盜竊金額。
    當選擇偷節點ii時,能獲得最大盜竊金額爲左右孩子都選擇不偷時獲得的最大盜竊金額之和加上節點ii的所藏金額。
    (3)確定起始
    節點爲空時,不管是選擇偷還是不偷,獲得最大盜竊金額都爲0.
    rootroot爲空,dp[root][0]=0,dp[root][1]=0dp[root][0]=0,dp[root][1]=0
    (4)確定終止
    根節點偷或不偷時的獲得最大盜竊金額。
    max{dp[Root][0],dp[Root][1]}max\{dp[Root][0],dp[Root][1]\}
class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> ans = robhelper(root);
        return max(ans[0],ans[1]);
    }
    vector<int> robhelper(TreeNode* root){
        vector<int> status(2,0);
        if(root == NULL){
            return status;
        }
        vector<int> lstatus = robhelper(root->left);
        vector<int> rstatus = robhelper(root->right);
        status[1] = lstatus[0] + rstatus[0] + root->val;
        status[0] = max(lstatus[0],lstatus[1]) + max(rstatus[0],rstatus[1]);
        
        return status;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章