区间动态规划例题:最优二叉树合并问题(带合并顺序约束)
题目描述
我们有一组带权叶子节点,需要将它们合并成一棵二叉树。
合并规则:每次只能合并相邻的两棵子树(或单个节点)为一棵新树,新树的权值为其左右子树权值之和,合并成本等于新树的权值。
此外,存在一个约束:某些叶子节点不能作为其他节点的左孩子(或右孩子)。
目标:在所有可能的合并顺序中,找出最小总合并成本。
输入:
- 叶子数量
n和权值数组w[1..n] - 一个布尔数组
left_forbidden[i]表示节点i能否作为左孩子(True表示禁止) - 一个布尔数组
right_forbidden[i]表示节点i能否作为右孩子(True表示禁止)
输出:最小总合并成本。
例子
n = 4
w = [1, 2, 3, 4]
left_forbidden = [False, True, False, False] # 节点2不能当左孩子
right_forbidden = [False, False, True, False] # 节点3不能当右孩子
一种可能的合并顺序:
- 合并节点1(左)和节点2(右)为树A,成本=1+2=3,节点2是右孩子,允许。
- 合并树A(左)和节点3(右)为树B,成本=(1+2)+3=6,节点3是右孩子,但节点3禁止当右孩子 → 不允许!
所以这个顺序非法。
我们需要在所有合法顺序中找到最小成本。
解题思路
这是“石子合并”的变种,但加入了左右孩子约束。
在石子合并中,dp[l][r] 表示合并区间 [l,r] 的最小成本,转移时枚举分界点 k,将区间分成 [l,k] 和 [k+1,r] 合并。
这里我们需要知道合并后的树的根节点是谁,因为当这棵树作为孩子时,它的根节点必须满足左右孩子约束。
步骤1:状态定义
设 dp[l][r][x] 表示将叶子区间 [l, r] 合并成以节点 x 为根的二叉树的最小成本,其中 x 是原叶子节点之一(即最终树的根是某个叶子)。
为什么根必须是叶子节点?
因为合并时,新树的根是“最后一次合并”的根,而这次合并的左子树和右子树的根分别是两个叶子节点(或子树的根,但递归地,最终根是某个原始叶子)。
所以我们可以用叶子的编号来代表子树的根。
状态大小:n^3,n 最大可能 100 左右可接受。
步骤2:初始化
单个叶子 [i,i] 组成的树,根只能是 i,成本为0(没有合并)。
即:
dp[i][i][i] = 0
其他 dp[i][i][*] = 无效(用无穷大表示)
步骤3:状态转移
考虑区间 [l, r] 以 x 为根的树是如何合并来的。
它的最后一次合并一定是将左子树区间 [l, k] 和右子树区间 [k+1, r] 合并,左子树的根是某个节点 a,右子树的根是某个节点 b。
合并后的新树根可以是 a 或 b(取决于谁当新根)。
情况1:左子树的根 a 成为总根 x。
那么这意味着在合并时,左子树作为整体成为新树的根,右子树 b 成为新树的右孩子。
约束条件:
- 节点
b可以作为右孩子吗?即right_forbidden[b]必须为False。 - 节点
a就是最终的根x,所以a = x。
这种情况下的成本:
总成本 = dp[l][k][a] + dp[k+1][r][b] + (sum[l][k] + sum[k+1][r])
但 a=x,所以是:
dp[l][k][x] + dp[k+1][r][b] + (sum[l][k] + sum[k+1][r])
且 right_forbidden[b] == False
情况2:右子树的根 b 成为总根 x。
那么左子树 a 成为新树的左孩子,约束:
- 节点
a可以作为左孩子吗?即left_forbidden[a]必须为False。 - 节点
b就是最终的根x,所以b = x。
成本:
dp[l][k][a] + dp[k+1][r][x] + (sum[l][k] + sum[k+1][r])
且 left_forbidden[a] == False
我们枚举所有可能的 k, a, b 来更新 dp[l][r][x]。
注意:a 必须在 [l, k] 中,b 必须在 [k+1, r] 中,x 必须是 a 或 b。
步骤4:计算顺序
按区间长度 len = 2 to n 从小到大计算。
对每个区间 [l, r],枚举分界点 k,再枚举左根 a 和右根 b。
复杂度 O(n^4) 可能太高,但注意 a 只能是 [l, k] 中的某个叶子,b 只能是 [k+1, r] 中的某个叶子,所以是 O(n^4)。
若 n ≤ 50 可接受。
可优化:我们不必枚举所有 a, b,只需枚举可能的根。但这里先按直接方法。
步骤5:最终答案
所有叶子都可能成为最终树的根,所以答案是:
min_{x=1..n} dp[1][n][x]
即区间 [1, n] 以某个叶子 x 为根的最小成本。
举例演算(简化例子,n=3)
叶子:1(权1),2(权2),3(权3)
约束:节点2不能当左孩子,节点3不能当右孩子。
初始化:
dp[1][1][1]=0
dp[2][2][2]=0
dp[3][3][3]=0
其他 inf
前缀和 sum[1]=1, sum[2]=3, sum[3]=6。
长度=2:
区间[1,2]:
k=1,左[1,1]根a=1,右[2,2]根b=2。
情况1:a为总根x=1,需检查b=2能否当右孩子 → right_forbidden[2]==False? 假设为False(允许),则成本=0+0+(1+2)=3,dp[1][2][1]=3。
情况2:b为总根x=2,需检查a=1能否当左孩子 → left_forbidden[1]==False? 假设为True(禁止),则此情况非法。
所以 dp[1][2][1]=3, dp[1][2][2]=? 看其他分界点无,所以dp[1][2][2] 可能由其他 k 得到吗?这里 k 只有1,已算过。但若左根b=2为总根,则上面情况2不允许,所以 dp[1][2][2] 为 inf。
区间[2,3]:
k=2,左[2,2]根a=2,右[3,3]根b=3。
情况1:a=2为总根x=2,需检查b=3能否当右孩子 → right_forbidden[3]==True(禁止) → 非法。
情况2:b=3为总根x=3,需检查a=2能否当左孩子 → left_forbidden[2]==True(禁止) → 非法。
所以 dp[2][3][2]=inf, dp[2][3][3]=inf。
意味着区间[2,3]无法合并成以2或3为根的树?这不对,因为可能先合并其他顺序。注意:长度=2时只有一种合并方式,确实这里[2,3]因约束无法合并,所以这个区间在更大区间中可能通过不同合并顺序满足约束吗?不,长度=2的区间必须直接合并这两个叶子,约束禁止则确实无法合并。但题目中允许任何合并顺序,所以如果[2,3]不能直接合并,可以在更大区间中让它们和别的节点合并来避免冲突。
这意味着我们区间DP的定义需要允许根不是区间内叶子吗?不,根必须是某个原始叶子,但[2,3]的根可以是2或3,但直接合并[2,3]时,总根是2或3,另一子树的根是另一个叶子,必须满足约束。这里检查约束失败,所以dp[2][3][2]和dp[2][3][3]都无效。但dp[2][3][?] 可能通过先合并成其他结构?不,[2,3]内只有两个叶子,只能直接合并。所以确实不可行。
那么整个[1,3]的合并就必须避免直接合并[2,3],而是先合并[1,2]再合并[1,2]+3。
这体现了约束的影响。
步骤6:改进状态定义
上述方法在区间较小时可能因约束无法合并,导致大区间无法计算,因为大区间依赖小区间的解。
所以我们需要允许“以区间内某个叶子为根”的树可能不存在,但大区间仍然可以合并。
改进:
定义 dp[l][r][root] 表示区间 [l,r] 合并成的二叉树的根是 root(root 是 [l,r] 中某个叶子)的最小成本。
转移时,枚举最后一次合并的分界点 k,左子树根 a,右子树根 b,且 a 和 b 必须在对应子区间内。
合并后的新树根可以是 a 或 b,但必须满足:
- 如果新根是 a,则 b 必须能当右孩子(right_forbidden[b]==False)
- 如果新根是 b,则 a 必须能当左孩子(left_forbidden[a]==False)
这个逻辑正确,但若小区间内某种根的树不存在(成本无穷大),则大区间无法用它来合并。
所以对于上面例子[2,3],由于直接合并违反约束,所以 dp[2][3][2] 和 dp[2][3][3] 都是无穷大。
那怎么合并[1,3]?
区间[1,3]的分界点 k=1:
左[1,1]根a=1,右[2,3]根b=2或3。
如果右子树以b=2为根,但 dp[2][3][2] 无穷大,所以不可行。
如果右子树以b=3为根,dp[2][3][3] 无穷大,也不可行。
所以 k=1 不行。
k=2:左[1,2]根a=1或2,右[3,3]根b=3。
先看左[1,2]的根a=1时成本dp[1][2][1]=3,右b=3,若新根选a=1,需检查b=3能否当右孩子 → 禁止,不行。若新根选b=3,需检查a=1能否当左孩子 → 允许(假设left_forbidden[1]=False),则成本= dp[1][2][1] + dp[3][3][3] + (sum[1,2]+sum[3,3]) = 3 + 0 + (3+3) = 9,所以 dp[1][3][3] = 9。
若左[1,2]的根a=2,dp[1][2][2] 是无穷大,不可用。
所以最终 dp[1][3][3]=9,dp[1][3][1] 和 dp[1][3][2] 可能无穷大。
答案 min(dp[1][3][x]) = 9(根为3)。
步骤7:总结
算法步骤:
- 预处理前缀和 sum[] 用于快速计算区间权值和。
- 初始化 dp[i][i][i]=0,其他为无穷大。
- 按区间长度从小到大计算 dp[l][r][x]:
- 枚举分界点 k 从 l 到 r-1
- 枚举左子树的根 a 在 [l, k] 中,且 dp[l][k][a] 有限
- 枚举右子树的根 b 在 [k+1, r] 中,且 dp[k+1][r][b] 有限
- 如果 right_forbidden[b]==False,则新根可以是 a,更新 dp[l][r][a] = min(dp[l][r][a], dp[l][k][a] + dp[k+1][r][b] + sum[l][r])
- 如果 left_forbidden[a]==False,则新根可以是 b,更新 dp[l][r][b] = min(dp[l][r][b], dp[l][k][a] + dp[k+1][r][b] + sum[l][r])
- 最终答案 = min_{x} dp[1][n][x]
时间复杂度 O(n^4),空间复杂度 O(n^3)。
核心思想:在石子合并 DP 的基础上,增加一维记录最后树的根(原始叶子编号),并在合并时检查左右孩子约束。
这个例题结合了经典的区间 DP 和额外的树结构约束,适合深入理解状态设计如何应对合并顺序限制。