代码随想录 day42 动态规划part07 打家劫舍 III

337.打家劫舍 III

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

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

思路

再读题目, 只有一个入口,---根, 每栋房子有且只有一个 '父'房子与之相连。---类似一棵二叉树, 孩子和父亲只能偷一个

 1
2  3

状态: dp[i]: 以i节点为父节点的最大收益。
dp[root] = max(dp[root.left] + dp[root.right], dp[孙子节点] + root.val)

dp--就地更新root, root 的大小来自root.left、root.right 或者 下一层。所以root的父亲的值不会影响他自己。
---顺序: 左 右 根。
初始化: 对root == None: return 0

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

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

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

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

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

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

#暴力递归
代码如下:

class Solution:
    def rob(self, root):
        if root == None: return 0
        if root.left == None and root.right == None:return root.val

        val1 = root.val
        if root.left:
            val1 += self.rob(root.left.left) + self.rob(root.left.right)  # 不偷左孩子
        if root.right:
            val1 += self.rob(root.right.left) + self.rob(root.right.right)  # 不偷右孩子
        val2 = self.rob(root.left) + self.rob(root.right)  # 偷左右孩子
        return max(val1, val2)

时间复杂度:O(n^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
空间复杂度:O(log n),算上递推系统栈的空间
当然以上代码超时了,这个递归的过程中其实是有重复计算了。

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

记忆化递推

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

代码如下:

code python

class Solution:
    def __init__(self):
        self.dict1 = {}

    def rob(self, root):
        if root == None: return 0
        if root.left == None and root.right == None:return root.val
        if self.dict1.get(root) is not None:return root.val

        val1 = root.val
        if root.left:
            val1 += self.rob(root.left.left) + self.rob(root.left.right)  # 不偷左孩子
        if root.right:
            val1 += self.rob(root.right.left) + self.rob(root.right.right)  # 不偷右孩子
        val2 = self.rob(root.left) + self.rob(root.right)  # 偷左右孩子
        root.val = max(val1, val2)
        self.dict1[root] = max(val1, val2)
        return max(val1, val2)

class Solution:
    dict1 = {}
    def rob(self, root):
        if root == None: return 0
        if root.left == None and root.right == None:return root.val
        if self.dict1.get(root) is not None:return root.val

        val1 = root.val
        if root.left:
            val1 += self.rob(root.left.left) + self.rob(root.left.right)  # 不偷左孩子
        if root.right:
            val1 += self.rob(root.right.left) + self.rob(root.right.right)  # 不偷右孩子
        val2 = self.rob(root.left) + self.rob(root.right)  # 偷左右孩子
        root.val = max(val1, val2)
        self.dict1[root] = max(val1, val2)
        return max(val1, val2)
时间复杂度:O(n)
空间复杂度:O(log n),算上递推系统栈的空间

动态规划 树形dp

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

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

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

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

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

vector<int> robTree(TreeNode* cur) {
其实这里的返回数组就是dp数组。

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

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

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

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

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

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

if (cur == NULL) return vector<int>{0, 0};
这也相当于dp数组的初始化

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

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

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

代码如下:

// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); //// 中

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

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

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

代码如下:

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};
举例推导dp数组
以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导)

code python

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        # dp数组(dp table)以及下标的含义:
        # 1. 下标为 0 记录 **不偷该节点** 所得到的的最大金钱
        # 2. 下标为 1 记录 **偷该节点** 所得到的的最大金钱
        dp = self.traversal(root)
        return max(dp)

    # 要用后序遍历, 因为要通过递归函数的返回值来做下一步计算
    def traversal(self, node):

        # 递归终止条件,就是遇到了空节点,那肯定是不偷的
        if not node:
            return (0, 0)

        left = self.traversal(node.left)
        right = self.traversal(node.right)

        # 不偷当前节点, 偷子节点
        val_0 = max(left[0], left[1]) + max(right[0], right[1])

        # 偷当前节点, 不偷子节点
        val_1 = node.val + left[0] + right[0]

        return (val_0, val_1)

自己理解 写一遍

class Solution:
    #  动态规划 树形dp

    def traversal(self, node):
        if not node:
            return (0, 0)  # (不偷当前节点, 偷当前节点)

        left = self.traversal(node.left)
        right = self.traversal(node.right)

        # 不偷当前节点 偷左右子树 -- 这个情况下的最优可能是偷左右本身,也可能是不偷左右本身
        val0 = max(left) + max(right)

        #  偷当前节点, 不偷左、右子树
        val1 = node.val + left[0] + right[0]

        return (val0, val1)


    def rob(self, root: Optional[TreeNode]) -> int:
        res = self.traversal(root)
        return max(res)

小结

1. 对于不偷左右子树、偷当前 + 左右子树的孩子的定义细细琢磨

2. 树的结果的再利用---记忆递归

3. 树形dp的使用---(使用, 不使用)的定义

4. 终止条件的定义都是根据要求来,不是一层不变的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值