198.打家劫舍
链接:LeetCode198.打家劫舍
动态规划五部曲
- 确定dp数组以及下标含义。dp[i]表示小偷在不触动警报装置的情况下,偷窃0-i家房屋能够偷到的最高金额。
- 确定递推公式。小偷不能偷相邻的房屋,但是可以偷取相邻房屋以外的房屋,也就是小偷偷了第i家房屋只能偷取第j家房屋(0<=j<=i-2,i>=2)。dp[i] = max(dp[i],nums[i]+dp[j]);
- 数组初始化。dp[0]=nums[0],dp[1]=nums[1];
- 确定遍历顺序。从前往后开始遍历。
- 举例推导dp数组。
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==1) return nums[0];
if(nums.size()==2) return nums[0]>nums[1]?nums[0]:nums[1];
vector<int> dp(nums.size(),0);
dp[0] = nums[0];
dp[1] = nums[1];
int ans=nums[0]>nums[1]?nums[0]:nums[1];
for(int i=2;i<nums.size();++i){
for(int j=i-2;j>=0;--j){
dp[i] = max(dp[i],nums[i]+dp[j]);
}
ans = ans>dp[i]?ans:dp[i];
}
return ans;
}
};
213.打家劫舍||
链接:LeetCode213.打家劫舍||
相比于198.打家劫舍,本题多了一个难度,第一间房屋和最后一件房屋连在一起形成一个圆圈。如果选择偷了第一家,就不能偷最后一家;如果选择偷了最后一家就不能偷第一家。既然圈这么烦人,可以“破圈”,分两步去偷窃,首先偷取下标为0-len-2的房屋(其中len表示房屋的总个数)得到的最大金额为ans1,再次偷取下标为1-len-1的房屋得到的总金额为ans2。ans = max(ans1,ans2);
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==1) return nums[0];
if(nums.size()==2) return nums[0]>nums[1]?nums[0]:nums[1];
int ans1 = robb(nums,0,nums.size()-2);
int ans2 = robb(nums,1,nums.size()-1);
return ans1>ans2?ans1:ans2;
}
private:
int robb(vector<int>&nums,int st,int en){
int len = en-st+1;
if(len==1) return nums[st];
if(len==2) return nums[st]>nums[en]?nums[st]:nums[en];
vector<int> dp(len,0);
dp[0] = nums[st];
dp[1] = nums[st+1];
int ans=dp[0]>dp[1]?dp[0]:dp[1];
for(int i=st+2;i<=en;++i){
for(int j=i-2;j>=st;--j){
dp[i-st] = max(dp[i-st],nums[i]+dp[j-st]);
}
ans = ans>dp[i-st]?ans:dp[i-st];
}
return ans;
}
};
337.打家劫舍|||
暴力回溯–会超时
class Solution {
public:
/*
偷当前节点,就不能偷当前节点的孩子节点
不偷当前节点,就可以考虑偷当前节点的孩子节点。
*/
int rob(TreeNode* root) {
return backtracking(root);
}
private:
//返回参数:返回偷或者不偷当前节点获取的最大金额
int backtracking(TreeNode *cur){
if(!cur) return 0;
//偷当前节点
int val1 = cur->val;
if(cur->left) val1+= backtracking(cur->left->left) + backtracking(cur->left->right);
if(cur->right) val1+= backtracking(cur->right->left) + backtracking(cur->right->right);
//不偷当前节点
int val2 = backtracking(cur->left) + backtracking(cur->right);
return max(val1,val2);
}
};
超时的原因在于,在偷当前节点的情况下计算了当前节点左右孩子的左右孩子为头节点的子树;不偷当前节点的情况下计算了当前节点的左右孩子为头节点的子树。有重复计算,时间复杂度为O(2^n);
记忆化递归回溯
class Solution {
public:
/*
偷当前节点,就不能偷当前节点的孩子节点
不偷当前节点,就可以考虑偷当前节点的孩子节点。
*/
int rob(TreeNode* root) {
return backtracking(root);
}
private:
//记忆化递归
unordered_map<TreeNode*,int> ma;
//返回参数:返回偷或者不偷当前节点获取的最大金额
int backtracking(TreeNode *cur){
if(!cur) return 0;
//偷当前节点
if(ma[cur]) return ma[cur];
int val1 = cur->val;
if(cur->left) val1+= backtracking(cur->left->left) + backtracking(cur->left->right);
if(cur->right) val1+= backtracking(cur->right->left) + backtracking(cur->right->right);
//不偷当前节点
int val2 = backtracking(cur->left) + backtracking(cur->right);
ma[cur] = max(val1,val2);
return ma[cur];
}
};
时间复杂度为O(n),空间复杂度为O(logn)(系统栈所占用的空间)
动态规划–树形dp
首先确定dp数组的类型,每个节点都有两个状态,要么偷当前节点要么不偷。所以需要返回两个值,一个是偷取当前节点获取的最大金额,一个是不偷取当前节点获得的最大的金额。
递归三部曲
- 确定递归参数和返回值。递归参数为TreeNode*,返回值为vector;
- 终止条件。if(!cur) return vector {0,0};(也是数组初始化)
- 确定遍历顺序。后序遍历
class Solution {
public:
/*
偷当前节点,就不能偷当前节点的孩子节点
不偷当前节点,就可以考虑偷当前节点的孩子节点。
*/
int rob(TreeNode* root) {
vector<int> res = backtracking(root);
return res[0]>res[1]?res[0]:res[1];
}
private:
//下标为0表示偷,下标为1表示不偷
vector<int> backtracking(TreeNode *cur){
if(!cur) return vector<int>{0,0};
vector<int> left = backtracking(cur->left);
vector<int> right = backtracking(cur->right);
vector<int> ans{0,0};
//偷取当前节点,左右孩子不能偷
ans[0] = cur->val + left[1] + right[1];
//不偷当前节点,左右孩子考虑偷
ans[1] = max(left[0],left[1]) + max(right[0],right[1]);
return ans;
}
};
和“968.监控二叉树”很类似,都是在树上进行操作。都要根据前一个节点的状态来推导当前节点的状态,再根据相应的状态做出相应的反应。