弄明白样例是关键。
由 中序遍历 ,前序遍历 确定样例,二叉树形态。
样例分数计算如下:
f(2)=7
f(5)=1*7+5=12
f(5)=10
f(4)=1*10+2=12
f(3)=12*12+1=145
节点数n=5,二叉树,形态数量,计算过程如下:
f(0)=1
f(1)=1
节点数量为2,扣除根节点后,还剩1个节点,该节点在左子树,对应形态数量f(1)*f(0)=1,
该节点在右子树,对应形态数量f(0)*f(1)=1
f(2)=f(1)*f(0)+f(0)*f(1)=2
f(3)=f(2)*f(0)+f(0)*f(2)+f(1)*f(1)=5
f(4)=f(3)*f(0)+f(0)*f(3)+f(2)*f(1)+f(1)*f(2)=5+5+2+2=14
f(5)=f(4)*f(0)+f(0)*f(4)+f(3)*f(1)+f(1)*f(3)+f(2)*f(2)=14+14+5+5+4=42
n=5,形态数量42,每种形态对应节点放置P(5,5)=5*4*3*2*1=120,总的可能120*42=5040,故n=30枚举该题,肯定超时。
基本确认,最终树的形态,关于根节点,接近对称。
大致算法如下:
将节点分数自小到大排序,根节点放最小值,第二层自左到右,放第二小,第三小,第三层,自右到左,放第四小,第五小,第四层,自左到右,放第六小,第七小,依次类推。但感觉不好操作。
再想想。
首先,我们要做的就是设计状态,其实就是设计dp数组的含义,它要满足无后效性。
关注这个 左子树*右子树+根 我只要知道左子树分数和右子树分数和根的分数(已给出),不就可以了吗?管他子树长什么样!
所以,我们ff数组存的就是最大分数,怎么存呢?
我们发现:子树是一个或多个节点的集合。
那么我们可不可以开一个f[i][j]来表示节点i到节点j成树的最大加分呢?可以先保留这个想法(毕竟暂时也想不到更好的了)。
如果这样话,我们就来设计状态转移方程。
按照刚刚的设计来说的话,我们的答案就是f[1][n]了,那么我们可以从小的子树开始,也就是len,区间长度。有了区间长度我们就要枚举区间起点,i为区间起点,然后就可以算出区间终点j。很明显,区间动规。
通过加分二叉树的式子我们可以知道,二叉树的分取决于谁是根,于是我们就在区间内枚举根k。
特别的,f[i][i]=a[i]其中a[i]为第i个节点的分数。
因为是要求最大值,所以我们就可以设计出
f[i][j]=MAX(f[i][k-1]*f[k+1][j]+f[k][k])
请注意:f[i][k-1]左子树,k根,f[k+1][j]右子树,与题中给定的中序遍历完全对应。
于是乎,我们就自己设计出了一个dp过程,因为是顺着来的,所以很少有不成立的。
至于输出前序遍历,我们再设计一个状态root[i][j]来表示节点i到节点j成树的最大加分所选的根节点。
所以我们按照根->左->右的顺序递归输出即可。
样例最大值过程模拟如下:
f[1][1]=5,f[2][2]=7,f[3][3]=1,f[4][4]=2,f[5][5]=10
f[1][0]=1,f[2][1]=1,f[3][2]=1,f[4][3]=1,f[5][4]=1
len=1
以节点1为根节点f[1][2]=max(f[1][2],f[1][0]*f[2][2]+f[1][1])=12
以节点2为根节点f[1][2]=max(f[1][2],f[1][1]*f[3][2]+f[2][2])=12
以节点2为根节点f[2][3]=max(f[2][3],f[2][1]*f[3][3]+f[2][2])=8
以节点3为根节点f[2][3]=max(f[2][3],f[1][1]*f[3][2]+f[2][2])=12
......
ybt
通过
测试点 | 结果 | 内存 | 时间 |
测试点1 | 答案正确 | 620KB | 2MS |
测试点2 | 答案正确 | 632KB | 2MS |
测试点3 | 答案正确 | 628KB | 1MS |
测试点4 | 答案正确 | 616KB | 1MS |
测试点5 | 答案正确 | 640KB | 1MS |
LOJ
LUOGU
AC代码如下:
#include <bits/stdc++.h>
#define LL long long
#define maxn 35
using namespace std;
LL f[maxn][maxn];
int root[maxn][maxn];
void PreOrder(int lt,int rt){
if(lt>rt)return;
printf("%d ",root[lt][rt]);
PreOrder(lt,root[lt][rt]-1);
PreOrder(root[lt][rt]+1,rt);
}
int main(){
int n,lt,rt,len,k,i;
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%lld",&f[i][i]);
for(i=1;i<=n+1;i++)f[i][i-1]=1;//空子树
for(i=1;i<=n;i++)root[i][i]=i;
for(len=1;len<n;len++)
for(lt=1;lt<n;lt++){
rt=lt+len;
if(rt>n)break;
for(k=lt;k<=rt;k++)//以k为根节点
if(f[lt][rt]<f[lt][k-1]*f[k+1][rt]+f[k][k]){
f[lt][rt]=f[lt][k-1]*f[k+1][rt]+f[k][k];
root[lt][rt]=k;
}
}
printf("%lld\n",f[1][n]);
PreOrder(1,n);
printf("\n");
return 0;
}
该题翻看了思路,但代码却是独立编写。
该题习得什么?
与中序遍历匹配的树上区间动规
后续:
若题干,一开始给的是前序遍历,或后序遍历,那怎么处理?
其实,只要状态转移方程作变更,就可以了,本质还是区间动态规划。
题干中若是给 前序遍历f[i][j]=MAX(f[i+1][k]*f[k+1][j]+f[i][i])
题干中若是给 后序遍历f[i][j]=MAX(f[i][k]*f[k+1][j]+f[j][j])