神奇脑洞题解——[NOI2009]二叉查找树
题面完美的诠释了一个概念:Treap
别急着走,这道题和Treap没有半毛钱关系(好吧,其实还是有一点的)
首先提一下Treap的一个性质,中序遍历恒定不变。
众所周知,Treap是可以转的,但是不管怎么转,整个树最靠左的左儿子儿子一定是最小的,这个儿子的父亲一定是第二小,最靠左的右儿子一定是第三小的(如果有右儿子的话)。这样我们就可以发现,这棵Treap的中序遍历正是所有元素按照权值从小到大排列,而且恒定不变。
上面的性质适用于Treap和Splay(不知道是啥的小可爱们看这里)
好的,下面我们得到了一个序列,由于Treap的根是可以瞎改的,(题目中的也是可以瞎搞)在区间i-j中任何一个点都可以作为子树i-j的根(如果提取i或者j就会成为一条链,如果按照二分法重构这棵树,就可以使其变为一棵极为优美的二叉树(和完全二叉树很像,但是某些地方会少一个点)
发现了什么没?区间,序列,最优值,这是赤果果的DP暗示啊
但是看看数据范围,DP一定很复杂。
状态:DP[i][j][k]记为在区间i到j中,将其变为一棵树,且所有节点的权值都大于等于k的最小花费(把权值离散化一下不就可以用71*71*71的数组存下了)
切记,这里的权值除了刚开始确定前序遍历和现在在这里恶心你以外没有任何卵用。
那么,假定现在在区间内枚举一个根X,再枚举一个最小权值KEL,我们就很容易得到方程
dp[i][j][KEL]=min(dp[i][j][KEL],dp[i][x-1][KEL]+dp[x+1][j][KEL]+sum[j]-sum[i-1]+Changeofval)
当且仅当点x的权值大于等于KEL
有dp[i][j][KEL]=min(dp[i][j][KEL],dp[i][x-1][VALX]+dp[x+1][j][VALX]+sum[j]-sum[i-1])
这里的sum是访问次数的前缀和,至于贡献所乘的深度,在每一次区间DP的过程中某些点都会被算一遍(实际上方程的每一次转移就相当于生成一棵子树,那么某些点的深度相应变大,被加入的次数也增加)
OK,放代码
//由题可知,这棵树就是个Treap,回想一下之前的技能 //Treap的中序遍历恒定不变,而且中序遍历的结果一定就是所有元素按照数据值从小到大排序 //这样就可以从中序遍历的第1位考虑到第n位 //分析可知权值没意义,可以直接离散化到(1-N)以内 //那么,假定dp状态 //dp[i][j][k]为中序遍历的第i位到第j位全部计算,并且所有点权值均不小于K的代价 //显然,权值为K的点一定是由第i位到第j位组成的子树的根 //那么在i到j中枚举一个根节点x,于此同时x也是区间的合并点 //每次合并x的左右子树即可 #include<iostream> #include<cstdio> #include<algorithm> #include<cstring> using namespace std; int dp[75][75][75]; int sum[75];//中序遍历序列关于访问频度的前缀和 struct PE { int times,data,val; }; PE point[75]; int n,K; int val[75]; bool Function(PE a,PE b) { return a.data<b.data; } int main() { scanf("%d%d",&n,&K); for(int i=1;i<=n;i++) { scanf("%d",&point[i].data); } for(int i=1;i<=n;i++) { scanf("%d",&val[i]); point[i].val=val[i]; } for(int i=1;i<=n;i++) { scanf("%d",&point[i].times); } sort(val+1,val+1+n); sort(point+1,point+1+n,Function); int cnts=unique(val+1,val+1+n)-val-1;//减去初始的地址 for(int i=1;i<=n;i++) { point[i].val=lower_bound(val+1,val+1+cnts,point[i].val)-val; sum[i]=sum[i-1]+point[i].times; } memset(dp,0x3f3f3f3f,sizeof(dp)); for(int i=1;i<=n+1;i++) { //特殊处理根节点是区间起点或终点的情况(子树是一条链) memset(dp[i][i-1],0,sizeof(dp[i][i-1])); } for(int l=1;l<=n;l++) {//区间长度 for(int i=1;i<=n-l+1;i++) {//区间起点 int j=i+l-1;//终点 for(int x=i;x<=j;x++) {//枚举根 for(int kel=1;kel<=n;kel++) { //枚举根权 dp[i][j][kel]=min(dp[i][x-1][kel]+dp[x+1][j][kel]+sum[j]-sum[i-1]+K,dp[i][j][kel]); if(kel<=point[x].val) dp[i][j][kel]=min(dp[i][j][kel],dp[i][x-1][point[x].val]+dp[x+1][j][point[x].val]+sum[j]-sum[i-1]); } } } } printf("%d",dp[1][n][1]); return 0; }
完结撒花!!!