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

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))

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)

复杂度分析

  • 时间复杂度: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])

复杂度分析

  • 时间复杂度: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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值