树上dp

88 阅读6分钟

基本做法

  1. 从树上找到满足约束的多个集合,求 (1) 所有集合的个数 (2) 集合内满足该约束的最小代价/最大代价

常见的约束

  1. 树的直径问题: 树的直径 = 任何一个节点 + 任何两个子树形成的链。

求解方向

  1. 链长
  2. 链的点权(每个节点具备一个cost)
  3. 链的边权(每个边具备一个cost)

扩展: 从二叉树到多叉树

  1. 树上最大独立集

独立集的定义: 集合中的所有节点都不相邻。一棵树有很多这样的集合,需要我们找出最优的集合

这里,一个过程返回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))
  1. 树上最小支配集

支配集的定义是,该集合内所有节点,能覆盖到整棵树。假设定义一个节点能覆盖到所有相邻节点(父亲,所有的孩子)。同样的,一棵树在满足该约束下有很多支配集,要求我们返回满足约束的最优集合的结果。

定义

  1. 只要每个结点满足 {自己装摄像头, 被父亲监视, 被任何一个孩子监视} 三个状态之一,那么选择装摄像头的结点,就构成一个支配集(覆盖整棵树)
  2. 当自己装摄像头时,左右孩子的状态可以从 {自己装, 被父亲监视, 被任何一个子孩子监视} 得到,为了找到更优的,哪个小要哪个。
  3. 当自己被父亲监视时,由于自己不安装摄像头,左右孩子的状态可以从 {自己装, 被任何一个子孩子监视} 得到,为了找到更优的,哪个小要哪个。
  4. 当自己被孩子监视时,由于自己不安装摄像头,并且至少有一个孩子安装摄像头,那么状态可以从{左孩子自己装,右孩子被其子孩子监视},{右孩子自己装,左孩子被其子孩子监视},{左孩子自己装,右孩子自己装}转移而来

对于(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)