树形DP——附LCCUP22题解
简介
树可以递归定义,树的每个子树也是一颗完整的树。
在树形动态规划当中,我们一般先算子树再进行合并,在实现上与树的 后序遍历 相似,都是先遍历子树,遍历完之后将子树的值传给父节点。简单来说我们动态规划的过程大概就是先递归访问所有子树,再在根上合并。
实例
LCCUP22-P3
思路
任意一个节点,会受到哪些因素影响:
- 其祖先节点的开关2的切换次数,奇数 = 切换,偶数 = 不切换 => bool表示
- 其父节点是否切换开关3 => bool表示
定义状态 (当前节点,祖先节点开关 2 的切换次数的奇偶性,父节点是否切换了开关 3),f(node, switch2, switch3) 每个状态表示从当前状态出发,最少需要操作多少次开关,可以关闭子树所有节点的灯。
判断当前节点灯的开启关闭——
开启:
- 初始灯 = 开,且switch2和switch3抵消(祖先节点开关2切换奇数次且父节点切换开关3 / 祖先节点开关2切换偶数次且父节点不切换开关3)
- 初始灯 = 关,且switch2和switch3不抵消(祖先节点开关2切换奇数次且父节点不切换开关3 / 祖先节点开关2切换偶数次且父节点切换开关3)
关闭:
- 初始灯 = 开,且switch2和switch3不抵消
- 初始灯 = 关,且switch2和switch3抵消
进一步简化,得到判断表达式:
(node.val == 1) == (switch2 == switch3)
如果当前受到祖先节点的开关影响后,变成开灯状态,那么可以操作一个或三个开关:
- 操作开关 1;
- 操作开关 2;
- 操作开关 3;
- 操作开关 123;
四种操作取最小值。
如果变成关灯状态,那么可以操作零个或两个开关:
- 不操作任何一个开关;
- 操作开关 12;
- 操作开关 13;
- 操作开关 23;
- 这四种操作取最小值。
同时需要用记忆化搜索进行剪枝。
时间复杂度——
O(状态个数) × O(单个状态的转移个数) = O(4n) × O(8) = O(n)
代码
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def closeLampInTree(self, root: TreeNode) -> int:
@cache # 记忆化搜索
def f(node: TreeNode, switch2: bool, switch3: bool) -> int:
if node is None:
return 0
if (node.val == 1) == (switch2 == switch3): # 当前节点为开灯
res1 = f(node.left, switch2, False) + f(node.right, switch2, False) + 1
res2 = f(node.left, not switch2, False) + f(node.right, not switch2, False) + 1
res3 = f(node.left, switch2, True) + f(node.right, switch2, True) + 1
res123 = f(node.left, not switch2, True) + f(node.right, not switch2, True) + 3
return min(res1, res2, res3, res123)
else: # 当前节点为关灯
res0 = f(node.left, switch2, False) + f(node.right, switch2, False)
res12 = f(node.left, not switch2, False) + f(node.right, not switch2, False) + 2
res13 = f(node.left, switch2, True) + f(node.right, switch2, True) + 2
res23 = f(node.left, not switch2, True) + f(node.right, not switch2, True) + 2
return min(res0, res12, res13, res23)
return f(root, False, False)

浙公网安备 33010602011771号