leetcode - [动态规划] - 打家劫舍

103 阅读3分钟

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;
    }
};