通过中序遍历区间与二叉树子树一一对应的性质,建立 O(n^3) 的区间 DP 模型求最大加分,并通过记录各区间最优根节点递归输出前序遍历。
OJ: luogu
题目 ID: P1040
难度:普及+/提高-
标签:动态规划区间 DP二叉树
创建: 2026-10-23 23:04
更新: 2026-10-26 14:14
形式化题目
给定长度为
对于任意子树
- 若
为空,其加分定义为 ; - 若
为非空,设其根节点编号为 ( ),左子树为 ,右子树为 ,则加分为: - 若
仅由单个叶节点 构成,其加分为 。
需要输出:
- 整棵树的最大加分;
- 取得最大加分的二叉树的前序遍历序列。
暴力解法
思路
因为整棵树的中序遍历固定为
对于当前区间
- 根节点
的左子树对应中序区间 ; - 根节点
的右子树对应中序区间 。
采用纯递归函数 solve(l, r),枚举每个可能的根
代码
/**
* 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-23 23:21
* update_at: 2026-10-23 23:21
*/
// brute.cpp:小数据暴力解。
// 通过递归枚举所有合法的二叉树形态(即每次在当前中序区间 [l, r] 中枚举根节点 k),
// 不使用记忆化,直接递归计算每种划分方式的得分并求最大值,同时记录最优根以输出前序遍历。
// 搜索状态数对应卡特兰数 C_n,复杂度随 n 呈指数爆炸,仅适合验证小规模数据(如 n <= 8)。
#include <iostream>
#include <vector>
using namespace std;
int n;
long long a[35];
struct Result {
long long score;
vector<int> preorder;
};
// 纯暴力递归搜索,不使用 memo / dp
Result solve(int l, int r) {
if (l > r) {
return {1, {}};
}
if (l == r) {
return {a[l], {l}};
}
Result best = {-1, {}};
for (int k = l; k <= r; ++k) {
Result left_res = solve(l, k - 1);
Result right_res = solve(k + 1, r);
long long current_score = left_res.score * right_res.score + a[k];
if (current_score > best.score) {
best.score = current_score;
best.preorder.clear();
best.preorder.push_back(k);
best.preorder.insert(best.preorder.end(), left_res.preorder.begin(), left_res.preorder.end());
best.preorder.insert(best.preorder.end(), right_res.preorder.begin(), right_res.preorder.end());
}
}
return best;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n)) return 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
Result ans = solve(1, n);
cout << ans.score << "\n";
for (size_t i = 0; i < ans.preorder.size(); ++i) {
cout << ans.preorder[i] << (i + 1 == ans.preorder.size() ? "" : " ");
}
cout << "\n";
return 0;
}复杂度
- 时间复杂度:每个大小为
的区间都需要枚举根节点划分为两个更小的子区间,所有合法的二叉树形态数量等于卡特兰数 。搜索过程中不记忆化,总递归调用次数与二叉树形态总数同阶,为指数级 。当 稍大时(如 )会彻底超时,仅适用于小数据( )。 - 空间复杂度:递归栈深度为
,每次返回包含前序遍历的数组,额外空间开销为 。
瓶颈
暴力的核心瓶颈在于重叠子问题的大量重复计算。不同全局树形结构中,相同的子区间
正解
思路
1. 中序遍历与子区间的等价性
二叉树在中序遍历下的一个关键性质是:任意一棵子树所包含的所有节点,在中序遍历序列中必然对应一段连续的闭区间。
已知整棵树的中序遍历为
- 任意子树都唯一对应一个区间
( ); - 如果我们在子树区间
中选定节点 ( )作为该子树的根节点,根据中序遍历“先左子树、后根、再右子树”的顺序: - 根节点
的左子树对应中序区间 ; - 根节点
的右子树对应中序区间 。
- 根节点
这一划分方式穷尽了在当前区间内构造二叉子树的所有可能。
2. 最优子结构与无后效性
子树
3. 状态定义与转移方程
定义
枚举区间
下表列出样例序列
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| 1 | 5 | 12 | 13 | 25 | 145 |
| 2 | 7 | 8 | 15 | 85 | |
| 3 | 1 | 3 | 13 | ||
| 4 | 2 | 12 | |||
| 5 | 10 |
例如 main.cpp 的区间 DP 一致.
4. 边界条件与初始化
- 空子树:根据题意,空子树加分为
。即当 时,规定 。 通过这一设定,当根 (左子树为空)时, ,与题目规则完美契合;右子树为空时同理。 - 叶子节点:当
时,只有一个节点, 。
5. 记录最优决策与输出前序遍历
为了输出取得最大加分时的前序遍历(根
- 用辅助数组
记录在区间 取得最大值时选取的根节点编号 ; - 求解完成后,从全区间
开始递归输出: - 打印当前根
; - 递归打印左子树
; - 递归打印右子树
。
- 打印当前根
代码
/**
* 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-23 23:21
* update_at: 2026-10-23 23:21
*/
#include <iostream>
#include <vector>
using namespace std;
// 节点数 <= 30,分数 <= 100,最大答案 <= 4e9,需要 long long
int n;
long long a[35];
long long dp[35][35];
int root[35][35];
// 递归打印前序遍历
void print_preorder(int l, int r) {
if (l > r) return;
int k = root[l][r];
cout << k << " ";
print_preorder(l, k - 1);
print_preorder(k + 1, r);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n)) return 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
// 初始化:空子树加分为 1
for (int i = 1; i <= n + 1; ++i) {
for (int j = 0; j <= n + 1; ++j) {
if (i > j) dp[i][j] = 1;
}
}
// 单个节点的加分就是其自身权值,根为其自身
for (int i = 1; i <= n; ++i) {
dp[i][i] = a[i];
root[i][i] = i;
}
// 区间 DP:len 从 2 到 n
for (int len = 2; len <= n; ++len) {
for (int l = 1; l + len - 1 <= n; ++l) {
int r = l + len - 1;
dp[l][r] = -1;
for (int k = l; k <= r; ++k) {
long long left_score = dp[l][k - 1];
long long right_score = dp[k + 1][r];
long long score = left_score * right_score + a[k];
if (score > dp[l][r]) {
dp[l][r] = score;
root[l][r] = k;
}
}
}
}
cout << dp[1][n] << "\n";
print_preorder(1, n);
cout << "\n";
return 0;
}复杂度
- 时间复杂度:
- 区间 DP 总状态数为
个区间 ; - 每个区间转移需要枚举根节点
,转移耗时为 ; - 动态规划总耗时为
; - 递归打印前序遍历每个节点仅访问一次,耗时
; - 对于
, ,执行时间在 以内。
- 区间 DP 总状态数为
- 空间复杂度:需要存储
矩阵与 矩阵,空间复杂度为 ,内存开销微乎其微。
总结
加分二叉树是二叉树中序遍历性质与区间 DP 的典范结合。解题的关键在于识别出:二叉树的中序子树与连续子区间具有一一对应关系。通过枚举区间的根节点,将一个大树划分为两个不相交的左、右子树区间,建立起清晰的区间包含关系;同时借助辅助数组记录最优决策点,便能轻松地在 DP 结束后递归重构出树的前序遍历。