• Leetcode 337. 打家劫舍 III


    1.题目描述

    小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

    除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警

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

    在这里插入图片描述


    输入: root = [3,2,3,null,3,null,1]
    输出: 7
    解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
    在这里插入图片描述

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


    提示:

    • 树的节点数在 [1, $10^4$] 范围内
    • 0 <= Node.val <= $10^4$

    2.思路分析

    简化问题:一棵二叉树,树上的每个点都有对应的权值,每个点有两种状态(选中和不选中),问在不能同时选中有父子关系的点的情况下,能选中的点的最大权值和是多少。

    2.1 暴力递归

    • 首先要明确相邻的节点不能偷,也就是爷爷选择偷,儿子就不能偷了,但是孙子可以偷
    • 二叉树只有左右两个孩子,一个爷爷最多 2 个儿子,4 个孙子

    根据以上条件,可以得出单个节点的钱计算方法:

    4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构

    2.2 记忆化递归

    对于解法一,经过分析其实现,我们发现爷爷在计算自己能偷多少钱的时候,同时计算了 4 个孙子能偷多少钱,也计算了 2 个儿子能偷多少钱。这样在儿子当爷爷时,就会产生重复计算一遍孙子节点。

    由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value。

    2.3 动态规划

    从递归的方法中,我们可以发现,最终最大收益为4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱

    合中的最大值。

    即为 每个子树都有最优解:偷窃根节点 和 不偷窃根节点下的最优解。

    我们重新定义这个问题,每个节点可以选择偷或者不偷,那么相连节点不能一起偷,那么:

    • 如果当前节点选择偷窃时,左右子节点不选择偷;
    • 如果当前节点选择不偷时,左右子节点主要能获得最优解就行。

    定义数组存储两个状态,索引 0 表示不偷,索引 1 表示偷。那么每个节点能偷到最大金额可定义为:

    • 当前节点选择偷窃时,最大金额数 = 左子节点不偷能获得的最大金额 + 右子节点不偷能获得的最大金额 + 当前节点的金额
    • 当前节点选择不偷时,最大金额 = 左子节点能偷的最大金额 + 右子节点能偷的最大金额。

    状态转移公式为:

    # 当前节点不偷
    money[0] = max(rob(root.left)[0], rob(root.left)[1])
           + max(rob(root.right)[0], rob(root.right)[1])
    # 当前节点偷
    money[1] = rob(root.left)[0] + rob(root.right)[0] +  root.val
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.代码实现

    3.1 暴力递归(超时)

    # 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:
            # 存在重复计算的情况
            if not root:
                return 0
            money = root.val
            if root.left:
                money += (self.rob(root.left.left) + self.rob(root.left.right))
            if root.right:
                money += (self.rob(root.right.left) + self.rob(root.right.right))
            return max(money, self.rob(root.left) + self.rob(root.right))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.2 记忆化递推

    # 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:
            # 存储计算结果
            money_map = dict()
    
            def dfs(root: Optional[TreeNode]) -> int:
                if not root:
                    return 0
                # 如果存在于哈希表中,直接拿过来用
                if root in money_map:
                    return money_map[root]
                money = root.val
                if root.left:
                    # 偷窃根节点时,无法偷取子节点,那么偷窃子孙节点
                    money += dfs(root.left.left) +dfs(root.left.right)
                if root.right:
                    money += dfs(root.right.left) + dfs(root.right.right)
                result = max(money, dfs(root.left) + dfs(root.right))
                money_map[root] = result
                return result
            return dfs(root)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    复杂度分析

    • 时间复杂度:O(n)。
    • 空间复杂度:O(n)。

    3.3 动态规划

    # 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:
            def dfs(root: Optional[TreeNode]) -> List[int]:
                # [不偷当前节点, 偷当前节点]
                if not root:
                    return [0, 0]
                money = [0, 0]
                left = dfs(root.left)
                right = dfs(root.right)
                # 两个索引对应两种状态,索引 0 表示不偷,索引 1 表示偷
                # 当前节点不偷
                money[0] = max(left[0], left[1]) + max(right[0], right[1])
                # 当前节点偷
                money[1] = left[0] + right[0] + root.val
                return money
    
            result = dfs(root)
            return max(result[0], result[1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    复杂度分析

    • 时间复杂度:O(n)。
    • 空间复杂度:O(n)。

    参考:

    1.https://leetcode.cn/problems/house-robber-iii/solution/san-chong-fang-fa-jie-jue-shu-xing-dong-tai-gui-hu/

    2.https://pythontechworld.com/article/detail/338WcgSwSzyk

  • 相关阅读:
    Kafka消费者的线程安全问题和多线程实现
    ai批量剪辑矩阵无人直播一站式托管系统源头技术开发
    用python计算积分
    【计算机网络——物理层】
    【畅购商城】详情页模块之评论
    26-大数据架构为什么要用kafka
    698. 划分为k个相等的子集
    26、类型别名
    程序员必看的十步高分电影,强推第八部
    30岁生日收到公司的生日礼物,一份裁员通知,有人从此一蹶不振,而我逆风翻盘,重获新生~
  • 原文地址:https://blog.csdn.net/weixin_44852067/article/details/126660677