洛谷P4338 [ZJOI2018]历史(LCT,树形DP,树链剖分)

洛谷题目传送门

ZJOI的考场上最弱外省选手T2 10分成功滚粗。。。。。。

首先要想到30分的结论

说实话Day1前几天刚刚刚掉了SDOI2017的树点涂色,考场上也想到了这一点

想到了又有什么用?反正想不到最大的贡献是怎么推出来的

然后晚上心中怀着九条CNM看完了Solution.pdf

貌似对我这个蒟蒻来说也只有这一题可做了。。。。。。

已知书上每个点access的总次数,构造出一个顺序,最大化虚实边的切换总次数

其实如果能发现最优顺序的构造是没有后效性的话,问题便可以进一步简化

考虑每个点的子树。假设已经对所有子树中的点构造出了一个最优顺序(一个序列),那么一定不会和它的所有祖先的子树中的最优序列产生冲突。这个并不好证明,仔细想一想应该能发现。

于是就可以单独考虑每个点\(x\)。能对\(x\)的实子边产生影响的是x的所有子树和\(x\)本身(access(x)会使\(x\)没有实子边),每切换一次都会使答案\(+1\)。显然同一个子树中产生的影响是相同的。于是我们要让来自不同子树(或\(x\)本身)尽可能交替access。当没有某个子树(或\(x\)本身)的\(a\)总和过大时,可以构造出使得(除了第一次)每一次access都有贡献的方案。如果某个子树(或\(x\)本身)的\(a\)总和过大,大于所有子树总和的一半时,是不可以的,那个子树(或\(x\)本身)的某几次操作肯定不会有贡献。用数学公式大概表示每个点的贡献为(\(S\)为子树的\(a\)之和,\(c\)\(x\)的每个子树)

\[\min\{S_x-1,2*(S_x-\max\{a_x,\forall S_c\})\} \]

\(\max\{a_x,\forall S_c\}\gt {S_x+1\over2}\)\(\min\)取后者

用树形DP算出来就有30分了

那么怎样快速修改呢?

首先,对某个点的\(a\)加上一个值\(w\),只可能会影响该点到根的路径上的点的贡献。

因为是加一个值,所以假如这些点中某些点的子树\(S\)大于它父亲子树\(S\)的一半,那么\(S_x+w\)\(\max\{a_x,\forall S_c\}\)也会\(+w\),带入上式发现贡献是不变的!

看到某个子树S大于所有子树总和的一半,有没有想到树剖?树剖的轻重边就是这样划分的啊!(反正我这种蒟蒻就是想不到

同样对维护好每个点子树S的树进行轻重链剖分,某些点的子树a之和大于它父亲子树a之和的一半就连重边(否则连轻边),这样每个点至多有一个重儿子。类似树剖的证明,每个点到根的轻边总数(也就是我们可能会修改的点数)是\(\log\sum a\)级别的!

修改的复杂度也有保障啦!我们只要快速找到这些点就好了。用实链剖分维护子树信息即可(应该不能叫LCT吧,没有makeroot,link和cut,对于这个问题蒟蒻的LCT总结对LCT的概念也改了改,欢迎Julao们指正!)。全局保存ans,每次进行类access操作找到虚边,就让ans直接减去以前的贡献,加上w以后判断当前的情况决定是否要切换虚实边并让ans加上新的贡献即可。

为了方便计算以前的贡献,蒟蒻觉得可以保存一下以前贡献的类型(无非就三种,某子树过大、自己过大、都不是很大)算的时候就省去了一些判断的时间。

代码细节巨多,尤其是类access更新答案那部分。所以就算考场上想到了一些东西,我这种蒟蒻也未必写得出来吧!疯狂膜拜考场切T2的laofu爷Orzzzzzzzzzzzz!

#include<cstdio>
#define RG register
#define R RG int
#define I inline void
#define lc c[x][0]
#define rc c[x][1]
#define G ch=getchar()
typedef long long L;
const int N=400009,M=N<<1;
int f[N],c[N][2],he[N],ne[M],to[M];
L ans,a[N],si[N],s[N];
short tp[N];
bool r[N];
template<typename T>
I in(RG T&x){
	RG char G;
	while(ch<'-')G;
	x=ch&15;G;
	while(ch>'-')x*=10,x+=ch&15,G;
}
inline bool nroot(R x){
	return c[f[x]][0]==x||c[f[x]][1]==x;
}
I up(R x){
	s[x]=s[lc]+s[rc]+si[x]+a[x];
}
I rot(R x){
	R y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
	if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;
	up(f[w]=y);f[y]=x;f[x]=z;
}
I splay(R x){
	R y;
	while(nroot(x)){
		if(nroot(y=f[x]))
			rot((c[f[y]][0]==y)^(c[y][0]==x)?x:y);
		rot(x);
	}
	up(x);
}
void dp(R x){//dp预处理答案
	R y,i,mp=x;
	RG L mx=a[x];
	for(i=he[x];i;i=ne[i]){
		if(f[x]==(y=to[i]))continue;
		f[y]=x;dp(y);
		si[x]+=s[y];
		if(mx<s[y])mx=s[y],mp=y;
	}
	if(mx<<1>(s[x]=si[x]+a[x])){
		ans+=(s[x]-mx)<<1;
		if(x!=mp)si[x]-=s[rc=mp];//子树过大
		else tp[x]=1;//自己过大
	}
	else tp[x]=2,ans+=s[x]-1;//都不是很大
}
int main(){
	R n,m,i,p=0,x,y;
	RG L w,S;
	in(n);in(m);
	for(i=1;i<=n;++i)in(a[i]);
	for(i=1;i<n;++i){
		in(x);in(y);
		to[++p]=y;ne[p]=he[x];he[x]=p;
		to[++p]=x;ne[p]=he[y];he[y]=p;
	}
	dp(1);printf("%lld\n",ans);
	while(m--){
		in(x);in(w);
		for(y=0;x;x=f[y=x]){
			splay(x);
			S=s[x]-s[lc];//算原来子树a总和,注意减s[lc]
			ans-=tp[x]<2?(S-(tp[x]?a[x]:s[rc]))<<1:S-1;
			S+=w;s[x]+=w;(y?si:a)[x]+=w;
			if(s[y]<<1>S)si[x]+=s[rc],si[x]-=s[rc=y];//虚实切换
			if(s[rc]<<1>S)   tp[x]=0,ans+=(S-s[rc])<<1;//子树过大
			else{
				if(rc)si[x]+=s[rc],rc=0;//没有子树过大,一定变虚
				if(a[x]<<1>S)tp[x]=1,ans+=(S-a[x])<<1;//自己过大
				else         tp[x]=2,ans+=S-1,rc=0;//都不是很大
			}
		}
		printf("%lld\n",ans);
	}
	return 0;
}
posted @ 2018-04-03 17:04  Flash_Hu  阅读(745)  评论(0编辑  收藏