题目链接
[NOIP2003 提高组] 加分二叉树
题目描述
设一个 n n n 个节点的二叉树 tree \text{tree} tree 的中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,…,n),其中数字 1 , 2 , 3 , … , n 1,2,3,\ldots,n 1,2,3,…,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i i i 个节点的分数为 d i d_i di, tree \text{tree} tree 及它的每个子树都有一个加分,任一棵子树 subtree \text{subtree} subtree(也包含 tree \text{tree} tree 本身)的加分计算方法如下:
subtree \text{subtree} subtree 的左子树的加分 × \times × subtree \text{subtree} subtree 的右子树的加分 + + + subtree \text{subtree} subtree 的根的分数。
若某个子树为空,规定其加分为 1 1 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,…,n) 且加分最高的二叉树 tree \text{tree} tree。要求输出
-
tree \text{tree} tree 的最高加分。
-
tree \text{tree} tree 的前序遍历。
输入格式
第 1 1 1 行 1 1 1 个整数 n n n,为节点个数。
第 2 2 2 行 n n n 个用空格隔开的整数,为每个节点的分数
输出格式
第 1 1 1 行 1 1 1 个整数,为最高加分( A n s ≤ 4 , 000 , 000 , 000 Ans \le 4,000,000,000 Ans≤4,000,000,000)。
第 2 2 2 行 n n n 个用空格隔开的整数,为该树的前序遍历。
样例 #1
样例输入 #1
5
5 7 1 2 10
样例输出 #1
145
3 1 2 4 5
提示
数据规模与约定
对于全部的测试点,保证 1 ≤ n < 30 1 \leq n< 30 1≤n<30,节点的分数是小于 100 100 100 的正整数,答案不超过 4 × 1 0 9 4 \times 10^9 4×109。
算法思想
最高加分
根据题目描述:
- 一棵二叉树的中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,…,n)。
在中序遍历中,一旦确定了根结点,那么左右子树的节点编号一定在根结点两侧,例如当根节点为 3 3 3时,那么左子树的结点编号为 1 , 2 1,2 1,2,右子树的结点编号为 4 , 5 , … , n 4,5,\ldots, n 4,5,…,n,如下图所示:
- 加分计算方法为左子树的加分 × \times × 右子树的加分 + + +根的分数。
求一棵符合中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,…,n) 且加分最高的二叉树,就是求以根节点为中心,将左右两个区间(子树)合并在一起的最大值,因此可以使用区间型动态规划进行处理。
状态表示
f [ i , j ] f[i,j] f[i,j]表示二叉树中序遍历的节点编号在区间 [ i , j ] [i,j] [i,j]的最大加分分值。
状态计算
从最后一个合并位置,也就是根节点的位置可以将状态计算分为下面几种情况:
- 根节点在 i i i位置,此时左子树为空,加分为 1 × 1\times 1×,得到的分数为 1 × f [ i + 1 ] [ j ] + w [ i ] 1\times f[i+1][j]+w[i] 1×f[i+1][j]+w[i]。
- 根节点在 i + 1 i+1 i+1位置,得到的分数为 f [ i ] [ i ] × f [ i + 2 ] [ j ] + w [ i + 1 ] f[i][i]\times f[i+2][j]+w[i+1] f[i][i]×f[i+2][j]+w[i+1]。
- …
- 根节点在 k k k位置,得到的分数为 f [ i ] [ k − 1 ] × f [ k + 1 ] [ j ] + w [ k ] f[i][k-1]\times f[k+1][j]+w[k] f[i][k−1]×f[k+1][j]+w[k]。
- …
- 根节点在 j j j位置,此时右子树为空,加分为 1 × 1\times 1×,得到的分数为 f [ i ] [ j − 1 ] × 1 + w [ j ] f[i][j-1]\times 1+w[j] f[i][j−1]×1+w[j]。
这里 w [ i ] w[i] w[i]表示第 i i i个节点的分数。 f [ i ] [ j ] f[i][j] f[i][j]要取以上情况的最大值。
初始状态
- 空树其加分为 1 1 1,也就是说 f [ i ] [ i − 1 ] = 1 f[i][i-1]=1 f[i][i−1]=1(或者 f [ i + 1 ] [ i ] = 1 f[i+1][i]=1 f[i+1][i]=1)
- 如果区间只有一个节点,那么分值就是当前节点的分数,即 f [ i ] [ i ] = w [ i ] f[i][i]=w[i] f[i][i]=w[i]
时间复杂度
状态数为 n × n n\times n n×n,状态计算需要枚举根节点的位置 1 1 1 ~ n n n,时间复杂度为 O ( n 3 ) O(n^3) O(n3)。
前序遍历
为了找到最大加分的前序遍历,就要在区间 [ i , j ] [i,j] [i,j]中找到一个根节点 k k k使得 f [ i ] [ k − 1 ] × f [ k + 1 ] [ j ] + w [ k ] f[i][k - 1]\times f[k+1][j]+w[k] f[i][k−1]×f[k+1][j]+w[k]等于 f [ i ] [ j ] f[i][j] f[i][j]。
对于前序遍历要先输出根节点 k k k,然后在递归遍历左子树( [ i , k − 1 ] [i,k-1] [i,k−1])和右子树( [ k + 1 , j ] [k+1,j] [k+1,j])即可。
注意,如果 i i i和 j j j相等,说明是叶子节点,其子树的根节点就是自己。
代码实现
#include <iostream>
using namespace std;
const int N = 50;
int w[N], f[N][N];
int n;
//求区间[i,j]的前序遍历
void dfs(int i, int j)
{
if(i > j) return; //空二叉树
if(i == j) cout << i << " "; //叶子节点
else
{
//枚举根节点
for(int k = i; k <= j; k ++)
{
//如果以k为根节点取得加分最大值
if(f[i][j] == f[i][k - 1] * f[k + 1][j] + w[k])
{
cout << k << " "; //输出根节点
dfs(i, k - 1); //递归遍历左子树
dfs(k + 1, j); //递归遍历右子树
break;
}
}
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> w[i];
//空树的加分为1
for(int i = 1; i <= n + 1; i ++) f[i][i - 1] = 1;
//枚举合并长度
for(int len = 1; len <= n; len ++)
{
//枚举开始位置
for(int i = 1; i + len - 1 <= n; i ++)
{
int j = i + len - 1; //结束位置
if(len == 1) f[i][i] = w[i]; //初始状态
else
{
//枚举其它根节点的位置
for(int k = i; k <= j; k ++)
f[i][j] = max(f[i][j], f[i][k - 1] * f[k + 1][j] + w[k]);
}
}
}
cout << f[1][n] << '\n';
dfs(1, n);
return 0;
}