动态规划四—打家劫舍问题

198. 打家劫舍

在这里插入图片描述
一、选择和状态
dp[i]状态:抢到第i个房间可抢劫的最大值x,要么就是i-1的最大值(选择不抢当前房间i),要么是i-2+i,而第i个房间在nums的值为nums[i-1]

解决动态规划问题就是找「状态」和「选择」,仅此而已。
假想你就是这个专业强盗,从左到右走过这一排房子,在每间房子前都有两种选择:抢或者不抢。

如果你抢了这间房子,那么你肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。

如果你不抢这间房子,那么你可以走到下一间房子前,继续做选择。

在两个选择中,每次都选更大的结果,最后得到的就是最多能抢到的 money:

// 主函数
int rob(vector<int>& nums){
	return dfs(nums, 0);
}

// 返回nums[start..] 能抢到的最大值
int dfs(vector<int>& nums, int start){
	if(start >= nums.length()) return 0;
	int res = max(
    			// 不抢去下家
    			dfs(nums, start+1),
    			// 抢,去下下家
    			nums[start]+dfs(nums, start+2)
				);
	return res;
}

明确了状态转移,就可以发现对于同一start位置,是存在重叠子问题的,比如下图:
在这里插入图片描述
盗贼有多种选择可以走到这个位置,如果每次到这都进入递归,岂不是浪费时间?所以说存在重叠子问题,可以用备忘录进行优化:

// 主函数
int rob(vector<int>& nums){
	for(int i =0; i<nums.size(); i++){
		memo.push_back(-1);
	}
	return dfs(nums, 0);
}

// 返回nums[start..] 能抢到的最大值
int dfs(vector<int>& nums, int start){
	if(start >= nums.length()) return 0;
	// memo备忘录,避免重复计算
	if(memo[start]!=-1) return memo[start];
	int res = max(
    			// 不抢去下家
    			dfs(nums, start+1),
    			// 抢,去下下家
    			nums[start]+dfs(nums, start+2)
				);
	// 记录备忘录
	memo[start]=res;
	return res;
}
private:
	vector<int> memo;

这就是自顶向下的动态规划解法,我们也可以略作修改,写出自底向上的解法:

参考地址_nettee

int rob(vector<int>& nums){
	int n = nums.size();
	// dp[i] = x 表示:
	// dp状态:当前位置i可抢劫的最大值:要么就是i-1的最大值(选择不抢当前房间i),要么是i-2+[i](选择抢当前房间)
	// base case: dp[0] = 0
	vector<int> dp(n+1);
	// base case
	dp[0] = 0;
	dp[1] = nums[0];
	for(int i =2; i<n;i++){
		// max(选第i间+dp[i-2], 选第i-1间)
		dp[i]=max(nums[i]+dp[i-2], dp[i-1]);
	}
	return dp[n-1];
}

我们又发现状态转移只和 dp[i] 最近的两个状态有关,所以可以进一步优化,将空间复杂度降低到 O(1)

int rob(vector<int>& nums){
	int n = nums.size();
	// dp[i] = x 表示:
	// dp状态:当前位置i可抢劫的最大值:要么就是i-1的最大值(选择不抢当前房间i),要么是i-2+[i](选择抢当前房间)
	// base case: dp[0] = 0
	//vector<int> dp(n+1);
	// base case
	//dp[0] = 0;
	int dp_i0=0;
	int dp_i1=nums[0];
	int dp_i;
	//dp[1] = nums[0];
	for(int i =2; i<=n;i++){
		// max(选第i间+dp[i-2], 选第i-1间)
		//dp[i]=max(nums[i]+dp[i-2], dp[i-1]);
		dp_i=max(nums[i-1]+dp_i0, dp_i1);
	}
	//return dp[n-1];
	return dp_i;
}
213. 打家劫舍 II

掐头去尾,将环形问题拆解成两个线性问题,即198的问题:

int rob(vector<int>& nums) {
      int n = nums.size();
      if(0==n) return 0;
      if(1==n) return nums[0];
      // 明确dp[i]为抢到第i个房子时能偷窃到的最高金额为dp[i], 而第i个房子的钱在nums中为nums[i-1]
      return max(dfs(nums, 0, nums.size()-1), dfs(nums, 1, nums.size())); // 抢[0, n-1]个房子 or [1, n]个房子,总之首尾不能同时抢
    }
    // 单个dfs就是198
    int dfs(vector<int>& nums, int start, int end){
      // base case
      // dp[i-2] = 0;
      int dp_i0 = 0;
      // 这个地方要注意,dp[1]=nums[0]想表达的意思
      int dp_i1 = nums[start];
      int dp_i;

      for(int i = start+2; i<=end;i++){
        //dp[i] = max(dp[i-1], dp[i-2]+nums[i-1]);
        dp_i = max(dp_i1, dp_i0+nums[i-1]);
        dp_i0 = dp_i1;
        dp_i1 = dp_i;
      }
      
      return dp_i;
    }
  1. 打家劫舍 III
    参考labuladong
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int rob(TreeNode* root) {
      if(root == NULL) return 0;
      // 利用备忘录消除重叠子问题
      if(memo.find(root)!=memo.end())
        return memo[root];
      
      // 思路没变,在root处抢还是不抢
      // 抢然后去下下家
      int do_it = root->val + 
                  (root->left==NULL?0:rob(root->left->left) + rob(root->left->right))
                  +
                  (root->right==NULL?0:rob(root->right->left) + rob(root->right->right));

      // 不抢然后去下家
      int not_do = rob(root->left)+rob(root->right);

      int res = max(do_it, not_do);

      memo[root] = res;
      return res;
    }

private:
  map<TreeNode*, int> memo;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值