● 198.打家劫舍
动规五部曲。
1、dp[j]含义。前j个房屋偷到的金额之和最大是dp[j]。
2、递推公式。递推公式要得出dp[i],就是要确定第i个房屋是否打劫,那么也跟之前的背包问题一样,放与不放,对应的是两种结果,我们只需要取这两种结果的最大值就行,而不需要用if-else语句来判断放和不放两种情况。
第i个房屋不被打劫,肯定前一个房屋要被打劫,所以此时dp[i]=dp[i-1]。
第i个房屋被打劫,肯定前一个房屋不能被打劫,所以dp[i]=dp[i-2]+nums[i]。因为i-1个房屋不考虑。
因此dp[i]是这两个数的最大值:dp[i]=max(dp[i-1], dp[i-2]+nums[i])
3、初始化。初始化要根据dp[j]含义来,上面递推公式涉及到前两个房屋,所以下标为0和 为1的房屋都要初始化。前1个房屋偷到的金额之和是nums[0],第2个房屋偷到的金额之和是max(nums[0],nums[1])。
或者说:只有1个房屋的话,就偷这一家,有2个房屋的话,偷其中金额大的那一家。
4、遍历顺序。到达第i个房屋,dp[i]需要根据前两个房屋的dp来,所以是正序遍历。
5、打印dp数组。
代码如下:
cpp
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==1)return nums[0];
vector<int> dp(nums.size(),0);
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
for(int i=2;i<nums.size();++i){
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp.back();
}
};
● 213.打家劫舍II
和上一题的区别,主要在于首元素、尾元素要不要考虑。上一题就是首尾元素以及中间的都考虑了,这道题的话首元素或者尾元素最多只有一个被选择,所以有下面三种情况:
情况1。首尾不选择,只考虑中间的。
情况2。
情况3。
是把这几种情况的最大金额分别算出来然后取最大值。
又因为情况2和情况3共有的部分包含了情况3,所以把情况2、情况3的最大金额分别算出来然后取最大值。
算最大值的过程就是和上一题一样,所以用两次上一题的过程,那么封装成一个函数。
代码如下。
cpp
class Solution {
public:
int subrob(vector<int>& nums,int a,int b){//只考虑[a,b]范围内的
vector<int> dp(nums.size(),0);
dp[a]=nums[a];
dp[a+1]=max(nums[a],nums[a+1]);
for(int i=a+2;i<=b;++i){
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[b];
}
int rob(vector<int>& nums) {
int n=nums.size();
if(n==1)return nums[0];
if(n==2)return max(nums[0],nums[1]);
int rob1=subrob(nums,0,nums.size()-2); //不考虑尾
int rob2=subrob(nums,1,nums.size()-1); //不考虑首
return max(rob1,rob2);
}
};
● 337.打家劫舍III
树形dp的入门题目。
要讨论某节点劫还是不劫,根据每个节点,我们只能知道它的两个孩子。如果这个节点选了,那么它的两个孩子就不能被打劫,这的两个孩子其实与上两道题的前一个房屋对应,所以只能后序遍历节点来解决,因为先要处理左右孩子,再根据孩子处理自己,左右中。
自己想不到的:不像之前的线性dp一样:一个dp数组,包含给定个数的元素。这里是遍历到一个节点了,就设置一个dp数组,这个数组里面2个元素:dp[0]是不选自己的最大值,dp[1]是选自己的最大值。主函数里面得到根节点的dp数组,然后返回根节点的dp[0]和dp[1]中的一个最大值就行,可见dp[0]和dp[1]是和前面dp[i]的max取值里面两个表达式含义相同。所以在递归体里面,我们要根据节点i左、右孩子的dp数组,得到i自己的的dp数组,一路向上,最终得到root的dp数组。
那么还是按递归的框架来,五部曲体现在递归体之中。
递归的返回类型应该是该节点的dp数组,告诉自己的父节点:不选自己打劫到的最大值和选择自己打劫到的最大值。父节点就根据左、右孩子的dp数组,来确定自己的dp数组。
就回到动规的递推公式,假设左孩子返回的dp数组是dpLeft,右孩子返回的dp数组是dpRight。首先看dp[1],选择了自己,那么左右孩子肯定不选,所以dp[1]应该等于不选左孩子打劫到的最大值dpLeft[0]加上不选右孩子打劫到的最大值dpRight[0],再加上自己的数,即:
cpp
dp[1]=dpLeft[0]+dpRight[0]+root->val;
再看dp[0],自己不选择,孩子选不选都可以。刚开始觉得孩子得左右都选,才是最大,但是这只是局部的最大,而不是全局。比如下面的结构:
正确结果应该是4、3,那么节点1的孩子2就没有选择。
既然左、右孩子选不选都可以,那我们应该选择左、右孩子选和不选两种情况的最大值然后加起来。所以dp[0]应该等于左孩子dp[0]和dp[1]的最大值,再加上右孩子dp[0]和dp[1]的最大值。
cpp
dp[0]=max(dpLeft[1],dpLeft[0])+max(dpRight[0],dpRight[1]);//没选自己,左右孩子选与不选都可以
代码如下:
cpp
/* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> digui(TreeNode* root){
if(!root)return vector<int>{0,0};//空节点
vector<int> dp(2,0);//dp[0];dp[1]
vector<int> dpLeft=digui(root->left);
vector<int> dpRight=digui(root->right);
dp[0]=max(max(dpLeft[1]+dpRight[1],dpLeft[0]+dpRight[1]),max(dpLeft[1]+dpRight[0],dpLeft[0]+dpRight[0]));//没选自己,可能左右孩子全选,也可能不选
dp[1]=dpLeft[0]+dpRight[0]+root->val;//选自己,孩子肯定不选
return dp;
}
int rob(TreeNode* root) {
vector<int> dp(2);
dp=digui(root);
return max(dp[0],dp[1]);
}
};
打印dp数组。按照每个节点得到dp[0]、dp[1]的过程先推导一下。