题目链接
文章链接
分析题干可知,当前房屋偷与不偷取决于前一个房屋和前两个房屋是否被偷。因此当前状态和前面状态会有一种依赖关系,这种依赖关系就是动规的递推公式。
利用动规五部曲进行分析:
1.确定dp数组及其下标的含义:
dp[i]:考虑下标i以内的房屋,最多可以偷窃的金额为dp[i]。
2.确定递推公式:
决定dp[i]的因素就是第i房间偷还是不偷。
如果偷第i间房间,那么dp[i] = dp[i - 2] + nums[i],因为第i - 1间房不用考虑,找到考虑下标i - 2以内的房屋,最多可以偷窃的金额dp[i - 2]再加上第 i 间房间偷到的钱;
如果不偷第i间房间,那么dp[i] = dp[i - 1],此时考虑第i间房偷到的最大金额与只考虑第 i - 1间房偷到的金额一样。
再取二者最大值,dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
3.dp数组初始化:
从递推公式可以看出循环遍历第条件是i > 2,因此要对dp[0]、dp[1]进行初始化。
dp[0] 一定是 nums[0],没有其他房屋可以选择,而dp[1] = max(nums[0], nums[1]),dp[1]可以在第一个和第二个房屋之间进行选择;
4.确定遍历顺序:
dp[i] 是根据dp[i - 2] 和dp[i - 1]推导出来的,那么一定是从前到后遍历。
5.打印dp数组:
以示例二,输入[2,7,9,3,1]为例。
class Solution {
public:
int rob(vector& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
vector dp(nums.size());
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[nums.size() - 1];
}
};
本题是打家劫舍类的入门题目,以下几道是本题的变形。
题目链接
文章链接
本题与上题的区别在于,本题的数组首尾相接,成环了。、
对于一个数组,成环的话主要有如下三种情况(直接借用代码随想录中的图):
情况一:考虑不包含首尾元
情况二:考虑包含首元素,不包含尾元素
情况三:考虑包含尾元素,不包含首元素
由于本题中考虑元素,但不一定选择,因此情况一其实包含在情况二或情况三中,所以只需要考虑后两种情况。
class Solution {
public:
int rob(vector& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
int result1 = robRange(nums, 0, nums.size() - 2); //情况二
int result2 = robRange(nums, 1, nums.size() - 1); //情况三
return max(result1, result2);
}
int robRange(vector& nums, int start, int end) {
if (end == start) return nums[start];
vector dp(nums.size());
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);
for (int i = start + 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[end];
}
};
题目链接
文章链接
本题把数据结构改成了树类型,对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。
本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。
与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子。
/**
* 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:
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); //不考虑左孩子
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); //不考虑右孩子
// 不考虑父节点
int val2 = rob(root->left) + rob(root->right);
return max(val1, val2);
}
};
当前代码超时,因为有许多重复计算,我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。
利用记忆化递推进行优化实现,使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。代码如下:
/**
* 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:
unordered_map umap; // 记录已经计算过的结果
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
if (umap[root]) return umap[root]; // 如果umap里已有记录,则直接返回
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); //不考虑左孩子
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); //不考虑右孩子
// 不考虑父节点
int val2 = rob(root->left) + rob(root->right);
umap[root] = max(val1, val2);
return max(val1, val2);
}
};
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,下面以递归三部曲为框架,并融合动规五部曲进行分析。
1.确定递归函数的参数和返回值:
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。参数为当前节点。
vector robTree(TreeNode* cur) {
里的返回数组就是dp数组。所以dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
所以本题dp数组就是一个长度为2的数组!在递归的过程中,系统栈会保存每一层递归的参数。
2.确定终止条件:
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
if (cur == NULL) return vector{0, 0};
也相当于dp数组的初始化。
3.确定遍历顺序:
在二叉树的遍历上是后序遍历;
4.确定单层递归的逻辑:
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
5.打印dp数组:
以示例1为例,dp数组状态如下:
最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱。
/**
* 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:
int rob(TreeNode* root) {
vector result = robTree(root);
return max(result[0], result[1]);
}
vector robTree(TreeNode* cur) {
if (cur == NULL) return vector {0, 0};
vector left = robTree(cur->left);
vector right = robTree(cur->right);
// 偷 cur,则不能偷左右节点
int val1 = cur->val + left[0] + right[0];
// 不偷 cur, 要考虑是否偷左右节点
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
今天的打家劫舍类题目比较新颖,刚接触确实有些不好上手,尤其是第三道二叉树与动规融合的题目,需要反复练习。