P1040 [NOIP 2003 提高组] 加分二叉树【题解】
P1040 [NOIP 2003 提高组] 加分二叉树
题目描述
设一个 nnn 个节点的二叉树 tree\text{tree}tree 的中序遍历为(1,2,3,…,n)(1,2,3,\ldots,n)(1,2,3,…,n),其中数字 1,2,3,…,n1,2,3,\ldots,n1,2,3,…,n 为节点编号。每个节点都有一个分数(均为正整数),记第 iii 个节点的分数为 did_idi,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 的根的分数。
若某个子树为空,规定其加分为 111,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 (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 的前序遍历。
输入格式
第 111 行 111 个整数 nnn,为节点个数。
第 222 行 nnn 个用空格隔开的整数,为每个节点的分数
输出格式
第 111 行 111 个整数,为最高加分($ Ans \le 4,000,000,000$)。
第 222 行 nnn 个用空格隔开的整数,为该树的前序遍历。
输入输出样例 #1
输入 #1
5
5 7 1 2 10
输出 #1
145
3 1 2 4 5
说明/提示
数据规模与约定
对于全部的测试点,保证 1≤n<301 \leq n< 301≤n<30,节点的分数是小于 100100100 的正整数,答案不超过 4×1094 \times 10^94×109。
解析&代码
OK,这题很简单,与其说是树形DP,倒不如说时区间DP更为贴切。
设 dpi,jdp_{i,j}dpi,j 表示区间 [i,j][i,j][i,j] 的节点建成一棵树时的最大加数。我们需要枚举根节点 k∈[i,j]k\in[i,j]k∈[i,j] 在哪里,再左右分别为两棵子树。而区间的两个端点为根实际上是一样的(反正都是一条链),所以特判即可。
而初始状态我们不难想到,就是 dpi,i=aidp_{i,i}=a_idpi,i=ai(aia_iai 为节点分数)
那么我们就能得到一个动态规划转移方程:
dpi,j=maxi≤k≤jdpi,k−1+dpk+1,jdp_{i,j}=\max_{i\leq k\leq j}dp_{i,k-1}+dp_{k+1,j}dpi,j=i≤k≤jmaxdpi,k−1+dpk+1,j
对于这个方程我们发现一个问题:当根节点取在区间两端时,即 k=ik=ik=i 或 jjj 时,k−1<ik-1<ik−1<i 或 k+1>jk+1>jk+1>j了,导致下标越界错误。那么我们就将根节点取在区间两端的情况单独拎出来计算,使i+1≤k≤j−1i+1\leq k\leq j-1i+1≤k≤j−1。而区间的两个端点为根实际上是一样的(反正都是一条链),所以只需处理一个端点即可。
所以最终方程为:
dpi,j=max(dpi+1,j+ai,maxi+1≤k≤j−1dpi,k−1+dpk+1,j)dp_{i,j}=\max(dp_{i+1,j}+a_i,\max_{i+1\leq k\leq j-1}dp_{i,k-1}+dp_{k+1,j})dpi,j=max(dpi+1,j+ai,i+1≤k≤j−1maxdpi,k−1+dpk+1,j)
我们像做普通 区间DP区间DP区间DP 一样,枚举所有区间,而最大加分结果就存在 dp1,ndp_{1,n}dp1,n 中
而对于前序输出的操作,我们只需再来一个数组 rootrootroot,rooti,jroot_{i,j}rooti,j 表示区间 [i,j][i,j][i,j] 的节点建成一棵加数最大的树时的根节点。初始状态 rooti,i=iroot_{i,i}=irooti,i=i 。最后递归输出即可。
具体操作见代码:
#include<bits/stdc++.h>
using namespace std;
int n;
int a[35];
int dp[35][35];
int root[35][35];
void dfs_cout(int l,int r){//递归实现前序输出if(l>r) return;cout<<root[l][r]<<" ";if(l==r) return;dfs_cout(l,root[l][r]-1);dfs_cout(root[l][r]+1,r);
}
int main(){cin>>n;for(int i=1;i<=n;i++){cin>>a[i];dp[i][i]=a[i];//你可以直接输入到dp[i][i]中,但这里为了照应上文好理解,单开了a[i]这个废物浪费空间root[i][i]=i;}for(int L=1;L<=n;L++){for(int i=1;i+L<=n;i++){int j=i+L;dp[i][j]=dp[i+1][j]+a[i];root[i][j]=i;//单独拎出根在左端点的情况,与根在右端点时分数一样,就不必再单独算右端点了for(int k=i+1;k<j;k++){if(dp[i][j]<dp[i][k-1]*dp[k+1][j]+a[k]){//发现更优dp[i][j]=dp[i][k-1]*dp[k+1][j]+a[k];//更新子树总分数root[i][j]=k;//根的改变} }}}cout<<dp[1][n]<<endl;dfs_cout(1,n);return 0;
}