0337.打家劫舍III - 图1
参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

337.打家劫舍 III

力扣题目链接

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

0337.打家劫舍III - 图2

思路

这道题目和 198.打家劫舍213.打家劫舍II也是如出一辙,只不过这个换成了树。

如果对树的遍历不够熟悉的话,那本题就有难度了。

对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。

本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算

与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。

如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”

暴力递归

代码如下:

  1. class Solution {
  2. public:
  3. int rob(TreeNode* root) {
  4. if (root == NULL) return 0;
  5. if (root->left == NULL && root->right == NULL) return root->val;
  6. // 偷父节点
  7. int val1 = root->val;
  8. if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了
  9. if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了
  10. // 不偷父节点
  11. int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
  12. return max(val1, val2);
  13. }
  14. };
  • 时间复杂度:0337.打家劫舍III - 图3#card=math&code=O%28n%5E2%29),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
  • 空间复杂度:0337.打家劫舍III - 图4#card=math&code=O%28%5Clog%20n%29),算上递推系统栈的空间

当然以上代码超时了,这个递归的过程中其实是有重复计算了。

我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。

记忆化递推

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

代码如下:

  1. class Solution {
  2. public:
  3. unordered_map<TreeNode* , int> umap; // 记录计算过的结果
  4. int rob(TreeNode* root) {
  5. if (root == NULL) return 0;
  6. if (root->left == NULL && root->right == NULL) return root->val;
  7. if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
  8. // 偷父节点
  9. int val1 = root->val;
  10. if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
  11. if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
  12. // 不偷父节点
  13. int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
  14. umap[root] = max(val1, val2); // umap记录一下结果
  15. return max(val1, val2);
  16. }
  17. };
  • 时间复杂度:0337.打家劫舍III - 图5#card=math&code=O%28n%29)
  • 空间复杂度:0337.打家劫舍III - 图6#card=math&code=O%28%5Clog%20n%29),算上递推系统栈的空间

动态规划

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

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

这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解

  1. 确定递归函数的参数和返回值

这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

参数为当前节点,代码如下:

  1. vector<int> robTree(TreeNode* cur) {

其实这里的返回数组就是dp数组。

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

所以本题dp数组就是一个长度为2的数组!

那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?

别忘了在递归的过程中,系统栈会保存每一层递归的参数

如果还不理解的话,就接着往下看,看到代码就理解了哈。

  1. 确定终止条件

在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

  1. if (cur == NULL) return vector<int>{0, 0};

这也相当于dp数组的初始化

  1. 确定遍历顺序

首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。

通过递归左节点,得到左节点偷与不偷的金钱。

通过递归右节点,得到右节点偷与不偷的金钱。

代码如下:

  1. // 下标0:不偷,下标1:偷
  2. vector<int> left = robTree(cur->left); // 左
  3. vector<int> right = robTree(cur->right); // 右
  4. // 中
  1. 确定单层递归的逻辑

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

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

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

代码如下:

  1. vector<int> left = robTree(cur->left); // 左
  2. vector<int> right = robTree(cur->right); // 右
  3. // 偷cur
  4. int val1 = cur->val + left[0] + right[0];
  5. // 不偷cur
  6. int val2 = max(left[0], left[1]) + max(right[0], right[1]);
  7. return {val2, val1};
  1. 举例推导dp数组

以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导

0337.打家劫舍III - 图7

最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱

递归三部曲与动规五部曲分析完毕,C++代码如下:

  1. class Solution {
  2. public:
  3. int rob(TreeNode* root) {
  4. vector<int> result = robTree(root);
  5. return max(result[0], result[1]);
  6. }
  7. // 长度为2的数组,0:不偷,1:偷
  8. vector<int> robTree(TreeNode* cur) {
  9. if (cur == NULL) return vector<int>{0, 0};
  10. vector<int> left = robTree(cur->left);
  11. vector<int> right = robTree(cur->right);
  12. // 偷cur
  13. int val1 = cur->val + left[0] + right[0];
  14. // 不偷cur
  15. int val2 = max(left[0], left[1]) + max(right[0], right[1]);
  16. return {val2, val1};
  17. }
  18. };
  • 时间复杂度:0337.打家劫舍III - 图8#card=math&code=O%28n%29),每个节点只遍历了一次
  • 空间复杂度:0337.打家劫舍III - 图9#card=math&code=O%28%5Clog%20n%29),算上递推系统栈的空间

总结

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

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

只不过平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解!

大家还记不记得我在讲解贪心专题的时候,讲到这道题目:贪心算法:我要监控二叉树!,这也是贪心算法在树上的应用。那我也可以把这个算法起一个名字,叫做树形贪心,哈哈哈

“树形贪心”词汇从此诞生,来自「代码随想录」

其他语言版本

Java

  1. class Solution {
  2. // 1.递归去偷,超时
  3. public int rob(TreeNode root) {
  4. if (root == null)
  5. return 0;
  6. int money = root.val;
  7. if (root.left != null) {
  8. money += rob(root.left.left) + rob(root.left.right);
  9. }
  10. if (root.right != null) {
  11. money += rob(root.right.left) + rob(root.right.right);
  12. }
  13. return Math.max(money, rob(root.left) + rob(root.right));
  14. }
  15. // 2.递归去偷,记录状态
  16. // 执行用时:3 ms , 在所有 Java 提交中击败了 56.24% 的用户
  17. public int rob1(TreeNode root) {
  18. Map<TreeNode, Integer> memo = new HashMap<>();
  19. return robAction(root, memo);
  20. }
  21. int robAction(TreeNode root, Map<TreeNode, Integer> memo) {
  22. if (root == null)
  23. return 0;
  24. if (memo.containsKey(root))
  25. return memo.get(root);
  26. int money = root.val;
  27. if (root.left != null) {
  28. money += robAction(root.left.left, memo) + robAction(root.left.right, memo);
  29. }
  30. if (root.right != null) {
  31. money += robAction(root.right.left, memo) + robAction(root.right.right, memo);
  32. }
  33. int res = Math.max(money, robAction(root.left, memo) + robAction(root.right, memo));
  34. memo.put(root, res);
  35. return res;
  36. }
  37. // 3.状态标记递归
  38. // 执行用时:0 ms , 在所有 Java 提交中击败了 100% 的用户
  39. // 不偷:Max(左孩子不偷,左孩子偷) + Max(又孩子不偷,右孩子偷)
  40. // root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) +
  41. // Math.max(rob(root.right)[0], rob(root.right)[1])
  42. // 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
  43. // root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
  44. public int rob3(TreeNode root) {
  45. int[] res = robAction1(root);
  46. return Math.max(res[0], res[1]);
  47. }
  48. int[] robAction1(TreeNode root) {
  49. int res[] = new int[2];
  50. if (root == null)
  51. return res;
  52. int[] left = robAction1(root.left);
  53. int[] right = robAction1(root.right);
  54. res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
  55. res[1] = root.val + left[0] + right[0];
  56. return res;
  57. }
  58. }

Python

暴力递归

  1. # Definition for a binary tree node.
  2. # class TreeNode:
  3. # def __init__(self, val=0, left=None, right=None):
  4. # self.val = val
  5. # self.left = left
  6. # self.right = right
  7. class Solution:
  8. def rob(self, root: TreeNode) -> int:
  9. if root is None:
  10. return 0
  11. if root.left is None and root.right is None:
  12. return root.val
  13. # 偷父节点
  14. val1 = root.val
  15. if root.left:
  16. val1 += self.rob(root.left.left) + self.rob(root.left.right)
  17. if root.right:
  18. val1 += self.rob(root.right.left) + self.rob(root.right.right)
  19. # 不偷父节点
  20. val2 = self.rob(root.left) + self.rob(root.right)
  21. return max(val1, val2)

记忆化递归

  1. # Definition for a binary tree node.
  2. # class TreeNode:
  3. # def __init__(self, val=0, left=None, right=None):
  4. # self.val = val
  5. # self.left = left
  6. # self.right = right
  7. class Solution:
  8. memory = {}
  9. def rob(self, root: TreeNode) -> int:
  10. if root is None:
  11. return 0
  12. if root.left is None and root.right is None:
  13. return root.val
  14. if self.memory.get(root) is not None:
  15. return self.memory[root]
  16. # 偷父节点
  17. val1 = root.val
  18. if root.left:
  19. val1 += self.rob(root.left.left) + self.rob(root.left.right)
  20. if root.right:
  21. val1 += self.rob(root.right.left) + self.rob(root.right.right)
  22. # 不偷父节点
  23. val2 = self.rob(root.left) + self.rob(root.right)
  24. self.memory[root] = max(val1, val2)
  25. return max(val1, val2)

动态规划

  1. # Definition for a binary tree node.
  2. # class TreeNode:
  3. # def __init__(self, val=0, left=None, right=None):
  4. # self.val = val
  5. # self.left = left
  6. # self.right = right
  7. class Solution:
  8. def rob(self, root: TreeNode) -> int:
  9. result = self.rob_tree(root)
  10. return max(result[0], result[1])
  11. def rob_tree(self, node):
  12. if node is None:
  13. return (0, 0) # (偷当前节点金额,不偷当前节点金额)
  14. left = self.rob_tree(node.left)
  15. right = self.rob_tree(node.right)
  16. val1 = node.val + left[1] + right[1] # 偷当前节点,不能偷子节点
  17. val2 = max(left[0], left[1]) + max(right[0], right[1]) # 不偷当前节点,可偷可不偷子节点
  18. return (val1, val2)

Go

动态规划

  1. func rob(root *TreeNode) int {
  2. res := robTree(root)
  3. return max(res[0], res[1])
  4. }
  5. func max(a, b int) int {
  6. if a > b {
  7. return a
  8. }
  9. return b
  10. }
  11. func robTree(cur *TreeNode) []int {
  12. if cur == nil {
  13. return []int{0, 0}
  14. }
  15. // 后序遍历
  16. left := robTree(cur.Left)
  17. right := robTree(cur.Right)
  18. // 考虑去偷当前的屋子
  19. robCur := cur.Val + left[0] + right[0]
  20. // 考虑不去偷当前的屋子
  21. notRobCur := max(left[0], left[1]) + max(right[0], right[1])
  22. // 注意顺序:0:不偷,1:去偷
  23. return []int{notRobCur, robCur}
  24. }

JavaScript

动态规划

  1. const rob = root => {
  2. // 后序遍历函数
  3. const postOrder = node => {
  4. // 递归出口
  5. if (!node) return [0, 0];
  6. // 遍历左子树
  7. const left = postOrder(node.left);
  8. // 遍历右子树
  9. const right = postOrder(node.right);
  10. // 不偷当前节点,左右子节点都可以偷或不偷,取最大值
  11. const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
  12. // 偷当前节点,左右子节点只能不偷
  13. const Do = node.val + left[0] + right[0];
  14. // [不偷,偷]
  15. return [DoNot, Do];
  16. };
  17. const res = postOrder(root);
  18. // 返回最大值
  19. return Math.max(...res);
  20. };

TypeScript

记忆化后序遍历

  1. const memory: Map<TreeNode, number> = new Map();
  2. function rob(root: TreeNode | null): number {
  3. if (root === null) return 0;
  4. if (memory.has(root)) return memory.get(root);
  5. // 不取当前节点
  6. const res1: number = rob(root.left) + rob(root.right);
  7. // 取当前节点
  8. let res2: number = root.val;
  9. if (root.left !== null) res2 += rob(root.left.left) + rob(root.left.right);
  10. if (root.right !== null) res2 += rob(root.right.left) + rob(root.right.right);
  11. const res: number = Math.max(res1, res2);
  12. memory.set(root, res);
  13. return res;
  14. };

状态标记化后序遍历

  1. function rob(root: TreeNode | null): number {
  2. return Math.max(...robNode(root));
  3. };
  4. // [0]-不偷当前节点能获得的最大金额; [1]-偷~~
  5. type MaxValueArr = [number, number];
  6. function robNode(node: TreeNode | null): MaxValueArr {
  7. if (node === null) return [0, 0];
  8. const leftArr: MaxValueArr = robNode(node.left);
  9. const rightArr: MaxValueArr = robNode(node.right);
  10. // 不偷
  11. const val1: number = Math.max(leftArr[0], leftArr[1]) +
  12. Math.max(rightArr[0], rightArr[1]);
  13. // 偷
  14. const val2: number = leftArr[0] + rightArr[0] + node.val;
  15. return [val1, val2];
  16. }

Go

  1. // 打家劫舍Ⅲ 动态规划
  2. // 时间复杂度O(n) 空间复杂度O(logn)
  3. func rob(root *TreeNode) int {
  4. dp := traversal(root)
  5. return max(dp[0], dp[1])
  6. }
  7. func traversal(cur *TreeNode) []int {
  8. if cur == nil {
  9. return []int{0, 0}
  10. }
  11. dpL := traversal(cur.Left)
  12. dpR := traversal(cur.Right)
  13. val1 := cur.Val + dpL[0] + dpR[0] // 偷盗当前节点
  14. val2 := max(dpL[0], dpL[1]) + max(dpR[0], dpR[1]) // 不偷盗当前节点
  15. return []int{val2, val1}
  16. }
  17. func max(a, b int) int {
  18. if a > b {
  19. return a
  20. }
  21. return b
  22. }

0337.打家劫舍III - 图10