Day48 | 198.打家劫舍, 213.打家劫舍II, 337.打家劫舍III(树形DP)
打家劫舍
LeetCode题目:https://leetcode.cn/problems/house-robber/
打家劫舍问题的初始问题,很明显,当存在i个房子的时候,打家劫舍的最大收益应该是多少?谈到最大收益,可以判断应该需要使用max来进行处理。同时,打家劫舍要求至少间隔一间房屋,所以递推公式需要考虑当前位置的临近位置,和间隔位置两种情况。因此可以看做是max(间隔房间 + 当前房间收益, 临近房间最大收益),共同来决定最大的收益。
因此,由以上推理,至少需要初始化两个值才能完成上述状态转移,其中最开始位置一定是当前房屋的收益。而存在两个房间的时候应该是收益最大值。
代码如下:
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[nums.size() - 1];
}
};
打家劫舍II
LeetCode题目:https://leetcode.cn/problems/house-robber-ii/
与问题1不同点在于,当存在环形的时候,开头的元素和末尾的元素是临近关系的。这就意味着会出现三种情况:(1)开头和末尾的元素都不需要。(2)需要开头的元素。(3)需要末尾的元素。其中(1)情况可以在2/3情况中包含计算。
因此,其实可以抽象看成,如果情况2,那就默认去掉末尾元素进行最大值计算。如果情况3,那就去掉开头元素进行最大值计算。最后再对两者进行比较,即可得到最后的结果。
最终代码如下:
class Solution {
public:
int rangeRob(vector<int>& nums, int begin, int end) {
vector<int> dp(nums.size(), 0);
dp[begin] = nums[begin];
dp[begin + 1] = max(nums[begin], nums[begin + 1]);
for (int i = begin + 2; i < end; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[end-1];
}
int rob(vector<int>& nums) {
if(nums.size() == 1) return nums[0];
if(nums.size() == 2) return max(nums[0], nums[1]);
int robFront = rangeRob(nums, 1, nums.size());
int robBack = rangeRob(nums, 0, nums.size() - 1);
return max(robFront, robBack);
}
};
打家劫舍III
LeetCode题目:https://leetcode.cn/problems/house-robber-iii/
该问题主要注重于对树形DP的理解,所谓树形DP,需要在二叉树上进行遍历并同时用返回值来保存状态的转移过程。因此核心在于对返回值的处理上,如何使返回值即表示当前节点的状态,又可以保存节点的值。可以参考DAY37 968.监控二叉树。
同时,要求得最大收益,因此每次遍历后要对遍历结果对比再进行返回,因此递归要采用后序遍历的方法,来进行递归以收集信息。此外,对于每个返回的值,因为打家劫舍要存在间隔,所以应该分为没有取当前节点的值,和取了当前节点的值两个情况来进行处理。因此,每次递归需要返回两个状态值,使用当前和没有使用当前,即val1 = root->val + leftDp[0] + rightDp[0],val2 = max(leftDp[0],leftDp[1]) + max(rightDp[0],rightDp[1]),最后返回(val1,val2)
如此,每次递归的流程便确定下来。最终代码如下:
class Solution {
public:
vector<int> robTree (TreeNode* root) {
if (root == NULL) return {0, 0};
vector<int> leftDp = robTree(root->left);
vector<int> rightDp = robTree(root->right);
int val1 = root->val + leftDp[0] + rightDp[0];
int val2 = max(leftDp[0], leftDp[1]) + max(rightDp[0], rightDp[1]);
return {val2, val1};
}
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
};
除了dp思路以外,也可以使用常规的递归模式进行处理,同时为了优化一些已知点的最大收益不被反复计算,应该设定map进行映射,以便于对节点是否被计算进行查询,如果已经被计算则直接返回值(理解上其实和状态转移差不多)
class Solution {
public:
unordered_map<TreeNode*, int> robMap;
int rob(TreeNode* root) {
if(root == NULL) return 0;
if(root->left == NULL && root->right == NULL) return root->val;
if(robMap.find(root) != robMap.end()) return robMap[root];
/* 计算当前节点 */
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);
robMap[root] = max(val1, val2);
return robMap[root];
}
};