中序固定使每棵子树必对应一段连续区间,于是按区间长度递增做区间 DP,枚举根合成 l×r+a,并记录每个区间的最优根以还原前序遍历,O(n^3)。
OJ: roj
题目 ID: 1580
难度:普及+/提高-
标签:动态规划区间二叉树python
创建: 2026-09-30 20:07
更新: 2026-10-07 13:50
形式化题目
给定
其中
求所有这些二叉树中加分的最大值,以及取得该最大值的一棵树的前序遍历。
以样例
graph TD
n3["3 (d=1)"] --> n1["1 (d=5)"]
n3 --> n4["4 (d=2)"]
n1 -.-> e1["空"]
n1 --> n2["2 (d=7)"]
n4 -.-> e2["空"]
n4 --> n5["5 (d=10)"]
这棵树的中序是
正解
思路
直接枚举为什么不行。 "中序遍历固定"意味着构造一棵树等价于反复做同一件事:在当前区间里挑一个节点当根,把它劈成左右两段。于是最直白的写法是递归
这个递归式本身完全正确,但它没有记忆化:同一个区间
瓶颈在哪里。 瓶颈是重复子问题。一个区间
状态怎么定。 令 score[l][r] 表示闭区间 score[l][k] 与 score[k+1][r],转移式与数学定义逐字对应,不用到处写 k-1 / k+1 的偏移。空区间此时恰好是 l == r。
为什么因子可以各自取最大。 转移式是
递推顺序。 按区间长度
下表是样例
score[l][r] |
|||||
|---|---|---|---|---|---|
| 5 | 12 | 13 | 25 | 145 | |
| — | 7 | 8 | 15 | 85 | |
| — | — | 1 | 3 | 13 | |
| — | — | — | 2 | 12 | |
| — | — | — | — | 10 |
对角线上的 score[0][5] = 145 就是答案。以它为例,枚举五个根的候选值分别是:
| 根 |
左因子 | 右因子 | 候选值 |
|---|---|---|---|
| 1 | 1 | 85 | 90 |
| 2 | 5 | 13 | 72 |
| 3 | 12 | 12 | 145 |
| 4 | 13 | 10 | 132 |
| 5 | 25 | 1 | 35 |
可以看到根
还原前序。 转移时顺手把取到最大值的 root[l][r]。求完表后从 root[0][n] 出发,按"根 → 左 → 右"展开即可。代码里用显式栈代替递归:先压右子树再压左子树,栈的后进先出保证弹出顺序就是前序。
叶子为什么要单独处理。 题面明确写"叶子的加分就是叶节点本身的分数,不考虑它的空子树"。若误把叶子也套进
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 20:07
# update_at: 2026-09-30 20:07
import sys
EMPTY_SCORE = 1 # 空子树的加分,即长度 0 区间的值
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
d = [next(data) for _ in range(n)] # d[i] 是节点 i+1 的分数,节点编号整体减一
# 区间用半开区间 [l, r) 表示,左右子树天然写成 [l, k) 与 [k+1, r)
# score[l][r]: 中序区间 [l, r) 组成的子树的最大加分
# root[l][r] : 该最优子树的根在中序里的下标,用于还原前序遍历
score = [[EMPTY_SCORE] * (n + 1) for _ in range(n + 1)] # 空区间(l == r)加分恒为 1
root = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(n): # 长度 1 的区间是叶子:加分就是自己的分数
score[i][i + 1] = d[i]
root[i][i + 1] = i
for length in range(2, n + 1): # 长度递增,保证两个子区间都已算完
for l in range(n - length + 1):
r = l + length
best = 0
for k in range(l, r): # 枚举根 k:左 [l, k)、右 [k+1, r)
cand = score[l][k] * score[k + 1][r] + d[k]
if cand > best:
best, root[l][r] = cand, k
score[l][r] = best
# 前序遍历 = 根 + 左子树 + 右子树;用栈展开,避免深递归
order: list[int] = []
stack = [(0, n)]
while stack:
l, r = stack.pop()
if l >= r:
continue # 空区间没有根
k = root[l][r]
order.append(k + 1) # 还原成 1-based 的节点编号
stack.append((k + 1, r)) # 栈后进先出:先压右子树,才能先弹出左子树
stack.append((l, k))
print(score[0][n])
print(' '.join(map(str, order)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 10:01
* update_at: 2026-10-05 10:01
*/
#include <cstdio>
#include <stack>
typedef long long ll;
const int MAXN = 35;
const ll EMPTY_SCORE = 1; // 空子树的加分,即长度 0 区间的值
int n;
ll d[MAXN]; // d[i]:中序第 i 个节点的分数(0-based 下标)
ll score[MAXN][MAXN]; // score[l][r]:中序半开区间 [l, r) 组成的子树的最大加分
int root[MAXN][MAXN]; // root[l][r]:该最优子树的根下标,用于还原前序遍历
// 前序遍历 = 根 + 左子树 + 右子树;用显式栈展开,避免深递归与递归实现细节
void print_preorder() {
std::stack<int> st_l;
std::stack<int> st_r;
st_l.push(0);
st_r.push(n);
bool first = true;
while (!st_l.empty()) {
int l = st_l.top();
int r = st_r.top();
st_l.pop();
st_r.pop();
if (l >= r) {
continue; // 空区间没有根
}
int k = root[l][r];
if (!first) {
printf(" ");
}
printf("%d", k + 1); // 还原成 1-based 的节点编号
first = false;
// 栈后进先出:先压右子树,才能先弹出左子树
st_l.push(k + 1);
st_r.push(r);
st_l.push(l);
st_r.push(k);
}
printf("\n");
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%lld", &d[i]);
}
// 空区间(l == r)加分恒为 1
for (int l = 0; l <= n; l++) {
for (int r = 0; r <= n; r++) {
score[l][r] = EMPTY_SCORE;
root[l][r] = 0;
}
}
// 长度 1 的区间是叶子:加分就是节点自己的分数,不套用 l*r+a
for (int i = 0; i < n; i++) {
score[i][i + 1] = d[i];
root[i][i + 1] = i;
}
// 按区间长度递增递推,保证左右两个子区间都已算好
for (int len = 2; len <= n; len++) {
for (int l = 0; l + len <= n; l++) {
int r = l + len;
ll best = 0;
for (int k = l; k < r; k++) {
// 枚举根 k:左子树 [l, k),右子树 [k+1, r)
ll cand = score[l][k] * score[k + 1][r] + d[k];
if (cand > best) {
best = cand;
root[l][r] = k;
}
}
score[l][r] = best;
}
}
printf("%lld\n", score[0][n]);
print_preorder();
return 0;
}复杂度
- 时间复杂度:
。状态 个,每个状态枚举 个根;前序还原额外 ,被主项吸收。 - 空间复杂度:
。 score与root各是一张的表。
总结
- 中序遍历固定为
是本题的题眼:它让"任意一棵子树"必然对应中序的一段连续区间,于是树的形状问题被压缩成区间问题。 - 枚举所有二叉树形状是卡特兰数级;瓶颈纯粹是重复子问题,加一层按区间记忆化即可降到
。 - 状态
score[l][r]用半开区间定义,可与转移式逐字对应;空区间(l == r)的值恰为,同时充当了空子树的边界。 - 因子能各自取最大,靠的是分数均为正数带来的乘法单调性,这条性质值得单独说清楚。
- 记录每个区间的最优根,就能在求完 DP 后用一次
的遍历还原前序遍历。
图示解析
前面的 Mermaid 图展示了样例的最优树结构,上面的两张表展示了状态含义与根枚举过程,二者合起来覆盖了"从区间状态到具体一棵树"的完整链路:score 表回答"最大加分是多少",root 表回答"这棵树长什么样"。