day48 | 198.打家劫舍、213.打家劫舍II、337.打家劫舍III

目录:

解题及思路学习

198.打家劫舍

https://leetcode.cn/problems/house-robber/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

思考:当前房间是否可以被偷盗取决于前面房间的状态。用动态规划的思路去想,当前房间存在被偷或者不被偷两种状态。

1、dp[i] 前i个房间被偷盗的最大金额。

2、 用一维数组,将其分为两个状态。

3、初始化: dp[0] = 0 , 非零下标初始为0.

4、遍历顺序:从前往后。

随想录:

1、dp[i] : 考虑下标i(包含) 偷取的最高金额 dp[i]

2、 分为偷取i 和不偷取i:

偷取i: dp[i-2] + nums[i]

不偷取i: dp[i - 1]

从这两个里面选取一个最大值。 dp[i] = max(dp[i - 2] + nums[i] , dp[i - 1])

3、从上面的公式可以看到,需要考虑前面两个的结果。所以需要初始化dp[0] 和dp[1]

dp[0] = 0; dp[1] = nums[0]; 其他初始化为0.

4、i是由i-1 和 i-2 确定的,所以要从前往后遍历。

for (i = 2; i < nums.size() ; i++)

5、打印

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQLUCNE9-1688976124536)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1fd5412-a9b6-4582-9097-e20a7465c0be/Untitled.png)]

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 0) return 0;
        if (nums.size() == 1) return nums[0];
        vector<int> 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];
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

感觉最重要的是分析传递公式的过程,如果不是很清晰的话 ,可以画图比较直观。

213.打家劫舍II

https://leetcode.cn/problems/house-robber-ii/

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

思考:相比第一道题,多了一个首尾相连的限制条件。所以,如果第一家偷了,就不能偷最后一家。第一家没被偷,就可以偷最后一家。得想办法知道dp[0] 的被偷盗状态,由此来判断最后一家是否偷盗。

随想录:对于一个数组,成环的话主要考虑三种情况:

1、考虑不包含首尾元素

2、考虑包含首元素,不包含尾元素

3、考虑包含尾元素,不包含首元素

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了

// 注意注释中的情况二情况三,以及把198.打家劫舍的代码抽离出来了
class Solution {
public:
    int rob(vector<int>& 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);
    }
    // 198.打家劫舍的逻辑
    int robRange(vector<int>& nums, int start, int end) {
        if (end == start) return nums[start];
        vector<int> dp(nums.size());
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[end];
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

我刚开始的思路是去记录第一家是否被使用。这种分情况讨论的方法,以后也可以多使用。

337.打家劫舍III

https://leetcode.cn/problems/house-robber-iii/

如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:

!https://assets.leetcode.com/uploads/2021/03/10/rob1-tree.jpg

输入:root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

思考:我记得之前做过一道类似的题目,当时是用回溯做的,给每个节点都分配了一个状态。

随想录:本题要使用后序遍历,因为通过递归函数的返回值来做下一步计算。

暴力递归:(会超时)

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); // 跳过root->left,相当于不考虑左孩子了
        if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了
        // 不偷父节点
        int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
        return max(val1, val2);
    }
};
  • 时间复杂度:O(n^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
  • 空间复杂度:O(log n),算上递推系统栈的空间

记忆化递推

可以使用一个map把计算结果保存一下,这样如果计算过孙子节点了,那么计算孩子的时候可以直接复用孙子节点的结果。

class Solution {
public:
    unordered_map<TreeNode* , int> 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); // 跳过root->left
        if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
        // 不偷父节点
        int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
        umap[root] = max(val1, val2); // umap记录一下结果
        return max(val1, val2);
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(log n),算上递推系统栈的空间

树形结构 动态规划

在上面两种方法,其实对一个节点 偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。

而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。 后序遍历。

单层递归逻辑:

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]);
    }
    // 长度为2的数组,0:不偷,1:偷
    vector<int> robTree(TreeNode* cur) {
        if (cur == NULL) return vector<int>{0, 0};
        vector<int> left = robTree(cur->left);
        vector<int> 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};
    }
};
  • 时间复杂度:O(n),每个节点只遍历了一次
  • 空间复杂度:O(log n),算上递推系统栈的空间

总结:

这道题是树形DP的入门题目,通过这道题目大家应该也了解了,所谓树形DP就是在树上进行递归公式的推导。

所以树形DP也没有那么神秘!

知识点记录

知识点

打家劫舍,分情况讨论。

树形dp。

个人反思

做题的时候将一些情况写下来更利于去推断公式。

你可能感兴趣的:(LeetCode,刷题,C++,算法,leetcode,数据结构,c++,动态规划)