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