题解:洛谷 P1040 加分二叉树
【题目来源】
洛谷:P1040 [NOIP 2003 提高组] 加分二叉树 - 洛谷
【题目描述】
设一个 \(n\) 个节点的二叉树 tree 的中序遍历为\((1,2,3,…,n)\),其中数字 \(1,2,3,…,n\) 为节点编号。每个节点都有一个分数(均为正整数),记第 \(i\) 个节点的分数为 \(d_i\),tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:
subtree 的左子树的加分 × subtree 的右子树的加分 + subtree 的根的分数。
若某个子树为空,规定其加分为 \(1\),叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 \((1,2,3,…,n)\) 且加分最高的二叉树 tree。要求输出
- tree 的最高加分。
- tree 的前序遍历。
【输入】
第 \(1\) 行 \(1\) 个整数 \(n\),为节点个数。
第 \(2\) 行 \(n\) 个用空格隔开的整数,为每个节点的分数
【输出】
第 \(1\) 行 \(1\) 个整数,为最高加分(\(Ans≤4,000,000,000\))。
第 \(2\) 行 \(n\) 个用空格隔开的整数,为该树的前序遍历。
【输入样例】
5
5 7 1 2 10
【输出样例】
145
3 1 2 4 5
【核心思想】
-
问题分析:给定 \(n\) 个节点的中序遍历为 \(1, 2, \dots, n\),每个节点有权值 \(d_i\)。一棵二叉树的加分定义为:左子树加分 \(\times\) 右子树加分 \(+\) 根节点分数(空子树加分为 1)。要求构造一棵中序遍历不变的二叉树,使其加分最大,并输出最大加分和前序遍历。本质是区间 DP,因为中序遍历固定,所以任意子树对应一个连续区间 \([i, j]\),其根节点为区间内某个节点 \(k\),左子树为 \([i, k-1]\),右子树为 \([k+1, j]\)。
-
算法选择:
- 区间 DP:\(dp[i][j]\) 表示区间 \([i, j]\) 构成子树的最大加分。\(root[i][j]\) 记录使 \(dp[i][j]\) 最大的根节点编号,用于构造前序遍历。
- 状态转移:对于区间 \([i, j]\),枚举根节点 \(k \in [i, j]\):
- 左子树加分:\(dp[i][k-1]\)(若 \(i>k-1\),则为 1)
- 右子树加分:\(dp[k+1][j]\)(若 \(k+1>j\),则为 1)
- 总加分:\(dp[i][k-1] \times dp[k+1][j] + d_k\)
- 取最大值,并记录对应的 \(k\)。
- 初始化:对于长度为 1 的区间,\(dp[i][i] = d_i\)(叶子节点加分即自身权值)。空子树加分视为 1,在转移时特殊处理。
-
关键步骤:
- 读入与初始化:读入 \(n\) 和 \(d[1..n]\),初始化 \(dp[i][i] = d[i]\)。
- 区间 DP:按长度 \(len\) 从 2 到 \(n\) 枚举:
- 枚举左端点 \(i\),计算右端点 \(j = i + len - 1\)。
- 初始化当前区间最大值为 \(dp[i][i] + dp[i+1][j]\)(根为 \(i\),左子树空视为 1,右子树为 \([i+1, j]\)),记录根为 \(i\)。
- 枚举根节点 \(k\) 从 \(i+1\) 到 \(j-1\):
- 左子树加分:\(dp[i][k-1]\)(若 \(i>k-1\),则为 1)
- 右子树加分:\(dp[k+1][j]\)(若 \(k+1>j\),则为 1)
- 计算当前加分 \(= dp[i][k-1] \times dp[k+1][j] + d_k\)。
- 若当前加分更大,更新 \(dp[i][j]\) 和 \(root[i][j]\)。
- 特殊处理根为 \(j\) 的情况(右子树空)。
- 输出:\(dp[1][n]\) 为最大加分;递归输出 \(root[1][n]\) 的前序遍历。
-
时间/空间复杂度:
- 时间复杂度:\(O(n^3)\),\(n \le 30\),完全可行。
- 空间复杂度:\(O(n^2)\),存储 DP 表和根节点记录。
-
区间 DP 的核心思想:
- 中序遍历固定:节点编号作为中序遍历顺序,使得任意子树对应一个连续区间,这是使用区间 DP 的前提。
- 枚举根节点:区间 DP 的典型策略——对于一个区间,枚举可能作为根的节点,将问题划分为左右两个独立的子区间(子树),利用最优子结构进行合并。
- 空子树处理:空子树加分为 1,这在转移时需特别注意,避免数组越界并保证乘法恒等元为 1。
- 记录构造:通过 \(root[i][j]\) 记录最优根节点,便于后续前序遍历的重建。
- 适用场景:适用于中序遍历固定、需要构造最优二叉树的场景,本质是卡特兰数相关的区间划分问题。
【算法标签】
普及+ #区间DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 定义int为long long类型
const int N = 35; // 定义最大节点数
int n; // 二叉搜索树的节点数量
int d[N]; // 存储每个节点的权值
int f[N][N]; // DP数组,f[i][j]表示区间[i,j]构成BST的最大分数
int root[N][N]; // 记录区间[i,j]的根节点位置
// 前序遍历输出BST结构
void dfs(int x, int y)
{
// 当区间长度为1时直接输出
if (x == y)
{
cout << x << ' ';
return;
}
// 区间不合法时返回
if (x > y) return;
// 输出当前区间的根节点
cout << root[x][y] << ' ';
// 递归遍历左子树
dfs(x, root[x][y] - 1);
// 递归遍历右子树
dfs(root[x][y] + 1, y);
}
signed main() // 使用signed替代int(因为定义了int为long long)
{
// 输入节点数量
cin >> n;
// 输入每个节点的权值并初始化DP数组
for (int i = 1; i <= n; i++)
{
cin >> d[i];
f[i][i] = d[i]; // 单个节点的分数就是其权值
}
// 动态规划处理所有区间
for (int len = 2; len <= n; len++) // 枚举区间长度
{
for (int i = 1; i + len - 1 <= n; i++) // 枚举区间起点
{
int j = i + len - 1; // 计算区间终点
// 情况1:i作为根节点
f[i][j] = f[i][i] + f[i + 1][j];
root[i][j] = i;
// 情况2:j作为根节点
int t = f[i][j - 1] + f[j][j];
if (t > f[i][j])
{
f[i][j] = t;
root[i][j] = j;
}
// 情况3:k作为根节点(i < k < j)
for (int k = i + 1; k < j; k++)
{
t = f[i][k - 1] * f[k + 1][j] + f[k][k];
if (t > f[i][j])
{
f[i][j] = t;
root[i][j] = k;
}
}
}
}
// 输出最大分数
cout << f[1][n] << endl;
// 前序遍历输出BST结构
dfs(1, n);
return 0;
}
【运行结果】
5
5 7 1 2 10
145
3 1 2 4 5
浙公网安备 33010602011771号