• 代码随想录 | Day 48 - LeetCode 198. 打家劫舍、LeetCode 213. 打家劫舍II、LeetCode 337. 打家劫舍III


            今天是打家劫舍系列,难度从前往后递增,只有第1道自己做出来了。第2、3道都是将DP作为解法的一部分,都有难度,尤其是第3题。第2道题要计算两个dp数组,再从中取较大值。第3道题有2种解法。第1种解法回顾了记忆化递归;第2种解法则是初次接触树形DP,将当前节点的dp数组作为返回值返回,供上一层的递归函数判断处理。


    第1题(LeetCode 198. 打家劫舍

            之前遇到过,这次自己解决了。dp[i]定义为从前i家房屋能获取的金额。状态转移方程方面,对于第2家房屋,只有打劫或不打劫两个选项。如果不打劫的话,就取上一个值dp[i - 1];而如果打劫的话,就意味着上一个房屋不在考虑范围内,所以要取上“上一个房屋对应的dp值”与“当前房屋价值”的和,即dp[i - 2] + nums[i]。目标是获取尽可能多的金额,所以从两者中取较大值,对应状态转移方程为dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])。初始化方面,对于前0个房屋,最优选择就是打劫第0个房屋,所以设置dp[0]为nums[0];对于前1个房屋,应该打劫第0、第1个房屋中价值较大的,所以设置为max(dp[0], dp[1])。每个点的值都依赖于其左方的2个值,所以从左向右遍历。

    1. class Solution {
    2. public:
    3. int rob(vector<int>& nums) {
    4. if (nums.size() == 1) {
    5. return nums[0];
    6. }
    7. vector<int> dp(nums.size());
    8. dp[0] = nums[0];
    9. dp[1] = max(nums[0], nums[1]);
    10. for (int i = 2; i < nums.size(); ++i) {
    11. dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
    12. }
    13. return dp.back();
    14. }
    15. };

            这一题的重点是dp[i]仅对应将第i个房屋考虑在内,而不一定会打劫第i个房屋,具体是否由状态转移方程来计算。

            二刷:dp数组不需要两行分别对应打劫了和没打劫,只需要一行即可。


    第2题(LeetCode 213. 打家劫舍II

            自己没做出来。这道题相比上一题,只增加了一个限制条件,就是房屋变成了环形排列,所以首尾房屋不能都打劫。那么就有两种选择,要么不考虑第一间房屋,要么不考虑最后一间房屋。然后再各自用上一题的解法得到结果,取两者的最大值作为最终答案。具体实现方法可以将上一题的方法抽象为函数,将数组的起始、末尾下标作为函数参数。

    1. class Solution {
    2. public:
    3. int robRange(vector<int>& nums, int indBegin, int indEnd) {
    4. if (indEnd == indBegin) {
    5. return nums[indBegin];
    6. }
    7. int len = indEnd - indBegin + 1;
    8. vector<int> dp(len);
    9. dp[0] = nums[indBegin];
    10. dp[1] = max(nums[indBegin], nums[indBegin + 1]);
    11. for (int i = 2; i < len; ++i) {
    12. dp[i] = max(dp[i - 1], dp[i - 2] + nums[indBegin + i]); // 不是nums[i]
    13. }
    14. return dp[len - 1];
    15. }
    16. int rob(vector<int>& nums) {
    17. if (nums.size() == 1) {
    18. return nums[0];
    19. }
    20. int ansFront = robRange(nums, 0, nums.size() - 2);
    21. int ansBack = robRange(nums, 1, nums.size() - 1);
    22. return max(ansFront, ansBack);
    23. }
    24. };

    函数的实现需要注意下标范围,比如第12行要注意nums和dp下标并不统一。题解的实现方法则是将dp数组大小设置为与nums一样,从而使得下标统一,不容易出错,虽然会多占用一个没用的int空间。具体实现是用下面的代码替换上面的第7~13行。

    1. vector<int> dp(nums.size());
    2. dp[indBegin] = nums[indBegin];
    3. dp[indBegin + 1] = max(nums[indBegin], nums[indBegin + 1]);
    4. for (int i = indBegin + 2; i <= indEnd; ++i) {
    5. dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
    6. }
    7. return dp[indEnd];

            二刷:忘记方法。 忘记对输入nums长度为1或2的判断。


    第3题(LeetCode 337. 打家劫舍III

            比较难,自己没想出来,题解有两种方法。第1种是递归解法。因为需要当前节点的“子树”或“孙子树”的返回值来决定当前节点的返回值,所以采用后序遍历。对于这道题,同样分为打劫当前点,和不打劫当前点两种情况。

            如果打劫当前节点,那么当前点的左右子节点就都不能被打劫了。所以应该考虑当前节点的孙子子树,也就是当前节点的“左子节点的左右子节点、右子节点的左右子节点”,这4个节点作为根节点的4棵树。将这4颗子树的返回值加起来,再加上当前节点的val,就得到打劫当前点所得到的金额。

            而如果不打劫当前节点,就应该考虑当前节点的左右子树,将两者的返回值相加作为结果。然后将这两种情况各自的结果取最大值,即是当前节点的返回值。

            递归的出口应该设置为遇到空节点就返回0。而为了减少不必要的递归,可以把叶子节点也设置为出口,返回叶子节点的val。而为了避免空指针问题,对于打劫当前节点的选择,在访问其孙子节点时,要保证其左或右子节点存在才能进行递归。

            在打劫当前节点的部分,已经对孙子节点进行了递归。但在不打劫当前节点部分,递归孩子节点的内部还会对孙子节点进行重复递归,所以直接用上面的方法会导致超时。解决方法是用day 46中第1题(LeetCode 139. 单词拆分)首次接触到的而记忆化递归。具体做法是设立一个map,用于存储每个节点的返回值。每次进入递归函数,都查询一下当前节点是否已经有了相应的返回值在map中。如果已经有了,就直接返回该值。没有的话,才进行分情况的递归,并在得到结果后将其放入map中。

    1. /**
    2. * Definition for a binary tree node.
    3. * struct TreeNode {
    4. * int val;
    5. * TreeNode *left;
    6. * TreeNode *right;
    7. * TreeNode() : val(0), left(nullptr), right(nullptr) {}
    8. * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    9. * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
    10. * };
    11. */
    12. class Solution {
    13. public:
    14. unordered_mapint> umap;
    15. int rob(TreeNode* root) {
    16. if (root == nullptr) {
    17. return 0;
    18. }
    19. if (root->left == nullptr && root->right == nullptr) { // 可不写,但会多一层递归
    20. return root->val;
    21. }
    22. if (umap[root]) {
    23. return umap[root];
    24. }
    25. // 打劫当前节点,并考虑当前节点的孙子子树
    26. int val1 = root->val;
    27. if (root->left) {
    28. val1 += rob(root->left->left) + rob(root->left->right);
    29. }
    30. if (root->right) {
    31. val1 += rob(root->right->left) + rob(root->right->right);
    32. }
    33. // 不打劫当前节点,并考虑当前节点的左右子树
    34. int val2 = rob(root->left) + rob(root->right);
    35. umap[root] = max(val1, val2);
    36. return max(val1, val2);
    37. }
    38. };

            第2种解法是树形DP解法。首先定义方面,将dp[0]和dp[1]分别定义为不打劫、和打劫某个节点,从“该节点作为根节点的树”中所能得到的金额。也就是说dp数组的总长度就是2,将dp数组作为递归函数的返回值。由于需要左右子节点各自打劫与不打劫的总共4个返回值,才能决定当前节点的两种情况对应的2个返回值,所以仍然需要后序遍历。递归出口方面,仍然是空结点返回0,因为空节点打劫与不打劫的金额都是0。

            而对于其他节点,在得到了左子树的返回值dpLeft和右子树的返回值dpRight后,就可以分不打劫、和打劫当前节点的两种情况来讨论。如果不打劫当前节点,那么其左右子结点是否被打劫就没有限制,取各自返回值的最大值再相加就可以,对应max(resLeft[0], resLeft[1]) + max(resRight[0], resRight[1])。而如果打劫当前节点,那么其左右子节点都不能被打劫,只能选择各自dp的第0位,再加上当前节点的val,对应cur->val + dpLeft[0] + dpRight[0]。

            得到这两种情况对应的结果后,再将两者作为返回值返回。然后在主函数中,取根节点返回值中两个数的较大值作为最终结果。

    1. class Solution {
    2. public:
    3. // vector第0、1位分别代表不打劫、打劫当前节点
    4. vector<int> robTree(TreeNode* cur) {
    5. if (cur == nullptr) {
    6. return {0, 0};
    7. }
    8. vector<int> dpLeft = robTree(cur->left);
    9. vector<int> dpRight = robTree(cur->right);
    10. int valNo = max(dpLeft[0], dpLeft[1]) + max(dpRight[0], dpRight[1]);
    11. int valYes = cur->val + dpLeft[0] + dpRight[0];
    12. return {valNo, valYes};
    13. }
    14. int rob(TreeNode* root) {
    15. vector<int> res = robTree(root);
    16. return max(res[0], res[1]);
    17. }
    18. };

            二刷:忘记方法。

  • 相关阅读:
    利用python的Matplotlib库进行基本绘图
    如何在本地上次文件到GitHub
    【Maven】SpringBoot多模块项目利用reversion占位符,进行版本管理.打包时版本号不能识别问题
    图形学中一些基本知识的总结与复习
    nodejs(二)http模块,创建最基本的web服务器,req请求对象,监听request事件,动态响应内容
    找不到x3daudio1_7.dll怎么解决?x3daudio1_7.dll的5个修复方法
    新版Testwell CTC++带来哪些新变化?
    cnpm安装步骤
    zookeeper安装部署和操作
    阿里P7,一个女测试经理的成长之路
  • 原文地址:https://blog.csdn.net/weixin_43469527/article/details/127737283