基本做法
- 从树上找到满足约束的多个集合,求 (1) 所有集合的个数 (2) 集合内满足该约束的最小代价/最大代价
常见的约束
- 树的直径问题: 树的直径 = 任何一个节点 + 任何两个子树形成的链。
求解方向
- 链长
- 链的点权(每个节点具备一个cost)
- 链的边权(每个边具备一个cost)
扩展: 从二叉树到多叉树
- 树上最大独立集
独立集的定义: 集合中的所有节点都不相邻。一棵树有很多这样的集合,需要我们找出最优的集合
这里,一个过程返回2个状态。
337 打家劫舍III
这里可以简化成
max(l, non_l) + max(r, non_r),真简练啊!和线性的打家劫舍一样,需要注意可能存在连续两个位置(在本题就是连续两层)都没偷的case。
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
# dfs(root): 返回两个值,表示偷这个root和不偷这个root产生的最大价值
# 重点: 为了避免过多的状态转移,可以用结果来表示状态,比如一个节点如果返回0,就表示没有偷这个节点
def dfs(root):
if root is None:
# 偷不偷都是0
return 0, 0
rob_left, non_rob_left = dfs(root.left)
rob_right, non_rob_right = dfs(root.right)
choose_root = non_rob_left + non_rob_right + root.val
non_choose_root = max(rob_left + rob_right, non_rob_left + rob_right, rob_left + non_rob_right, non_rob_left + non_rob_right)
return choose_root, non_choose_root
return max(dfs(root))
- 树上最小支配集
支配集的定义是,该集合内所有节点,能覆盖到整棵树。假设定义一个节点能覆盖到所有相邻节点(父亲,所有的孩子)。同样的,一棵树在满足该约束下有很多支配集,要求我们返回满足约束的最优集合的结果。
定义
- 只要每个结点满足 {自己装摄像头, 被父亲监视, 被任何一个孩子监视} 三个状态之一,那么选择装摄像头的结点,就构成一个支配集(覆盖整棵树)
- 当自己装摄像头时,左右孩子的状态可以从 {自己装, 被父亲监视, 被任何一个子孩子监视} 得到,为了找到更优的,哪个小要哪个。
- 当自己被父亲监视时,由于自己不安装摄像头,左右孩子的状态可以从 {自己装, 被任何一个子孩子监视} 得到,为了找到更优的,哪个小要哪个。
- 当自己被孩子监视时,由于自己不安装摄像头,并且至少有一个孩子安装摄像头,那么状态可以从{左孩子自己装,右孩子被其子孩子监视},{右孩子自己装,左孩子被其子孩子监视},{左孩子自己装,右孩子自己装}转移而来
对于(4),更通用的写法是(对于多叉树)
# 1. 获取所有 "选择在子孩子上安摄像头 和 选择子孩子被其子孩子监视" 得到的结果之差 里 最小的
m = max(0, min(child_i_monitor_by_root - child_i_monitor_by_child for child_i in children))
return m + sum(min(child_i_monitor_by_root, child_i_monitor_by_child) for child_i in children))
不过多解释,简单理解就是,既然必须要安装摄像头到一个孩子,那不妨安装在 "装或不装差异最小"的那个结点上。
比如有三个孩子。
A 装的代价是10,不装代价是3
B 装的代价是13,不装的代价是11
C 装的代价是10,不装的代价是15
那么,我们就装在C上。因为 max(0, min(7, 2, -5) = 0),而min(c_装, c_不装)出自装。
理解: 如果一个结点装比不装好,那就直接装这个结点上了。很显然。
A 装的代价是10,不装代价是3
B 装的代价是13,不装的代价是11
C 装的代价是10,不装的代价是4
如果这种情况下,谁装了都会导致结果变大,那就装在差异最小的节点上,也就是B。
# 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 minCameraCover(self, root: Optional[TreeNode]) -> int:
# dfs的返回值,表示 能够使得root被完全覆盖的三种情况下分别需要多少个摄像头
# 换句话说,dfs负责返回三种可能成立的最小支配集。
def dfs(root):
# 1. 定状态: 为了找到root的最小支配集,我们有
# 1.1 状态1: 当前节点上安装了摄像头
# 1.2 状态2: 当前节点的父亲上安装了摄像头
# 1.3 状态3: 当前节点有一个孩子安装了摄像头
# 上面三种状态,都可以满足当前节点被监控到,因此使得所有结点都处于三种状态之一,就能找到一个支配集。
# 3. 边界条件:
if root is None:
# 3.1 不可能在None上装摄像头
# 3.2, 3.3 None不需要被其他人监视,可以认为一颗空树的支配集代价恒为0
return inf, 0, 0
# 2. 从子过程转移而来: 看下左右孩子分别在处于三种状态下此时的支配集需要多少代价
l_monitor_by_root, l_monitor_by_father, l_monitor_by_child = dfs(root.left)
r_monitor_by_root, r_monitor_by_father, r_monitor_by_child = dfs(root.right)
# 2.1 如果在当前root装摄像头,那么子孩子装不装都无所谓
# 左孩子哪个状态下的支配集更优,就选哪个
# 右孩子哪个状态更优就选哪个
monitor_by_root = min(l_monitor_by_child, l_monitor_by_father, l_monitor_by_root) \
+ min(r_monitor_by_child, r_monitor_by_father, r_monitor_by_root) \
+ 1
# 2.2 如果root需要被父亲监视,也就是root上没有摄像头,那么子过程不可能被father监视
# 同样的,子过程无论被孩子监视,还是自己监视都无所谓,因为子过程不用管自己
monitor_by_father = min(l_monitor_by_child, l_monitor_by_root) + min(r_monitor_by_child, r_monitor_by_root)
# 2.3 如果root需要被孩子监视,那么孩子不可能被root监视,同时,必须要保证有一个孩子是由自己监视的
monitor_by_child = min(
l_monitor_by_child + r_monitor_by_root, # 摄像头在右孩子
l_monitor_by_root + r_monitor_by_child, # 摄像头在左孩子
l_monitor_by_root + r_monitor_by_root # 两个孩子都有摄像头
)
return monitor_by_root, monitor_by_father, monitor_by_child
# root没有父亲,相当于一个非法条件,我们找出root上安装摄像头和root上不安装摄像头的最优解
monitor_by_rt, _, monitor_by_child = dfs(root)
return min(monitor_by_rt, monitor_by_child)