[洛谷P4338][题解][ZJOI2018]历史

0.Description

原题面
简化版题意:给定每个点access次数(存在单点增加),求虚实边切换最大次数。

1.Solution

这道题看起来就是一个 LCT,但是它没有 LCT 常见的连边删边等操作,而是着重考察了对于access函数的理解与灵活运用。
观察题意,我们不难发现节点(u)对答案的贡献仅与它的子树有关:只要有和之前不同的儿子崛起,(u)就会改变。比较细心的同学可能已经发现它可以抽象成一个模型:给定(m)种颜色的球和出现次数(cnt_i),求最多有多少组相邻不同色球。
而这个模型有一个固定的解法:令(t=sum cnt_i,h=max{cnt_i}),则最终答案为(min{2(t-h),t-1}),也就是说当(2h>t)时答案为(2(t-h)),否则为(t-1)。这个式子有一个很好理解的解释:如果最多的球不过半,那么我们总能找到一种方案使得所有球都交替放;如果最多的球过半,我们就将剩余的球不相邻地插入最多的球,每插入一个贡献(2)的答案。
由此,我们就有了一种确定的方法来计算子树的贡献:如果在(u)的儿子中出现了(2*siz[v]>siz[u])的情况(以下简称垄断颜色),我们就连实边,否则连虚边。可以发现,每个点至多有一条实边且实边的贡献类型固定为(2(t-h))

接下来我们考虑:LCT 中需要维护什么信息?
对于一般的信息更新siz[k]=siz[ls]+siz[rs]+val[k],它维护的是 Splay 中的信息,现在我们想要维护原树中子树的信息,就需要多用一个vs[k]来维护原树中虚边的信息(这些信息是不记入 Splay 的),更新时则变为siz[k]=siz[ls]+siz[rs]+val[k]+vs[k]

接下来就到了最为关键的access操作,由于我们发现虚边的条数为(mathcal{O(log a_i)})(走虚边则值减半),所以可以考虑access跳虚边时暴力维护。维护时先将答案减去原值,然后注意垄断节点的变化,更新完毕后加回答案,有亿些细节可以看代码(我不会告诉你我把题解代码手抄下来回宿舍理解了一中午的(但是理解之后确实很快过了))。另外,LCT 中的一些信息(比如vssiz)和初始答案需要 DFS 时预处理出来。

2.Code

第二个点就爆int,你好狠心啊

const int N=400010;
int n,m,a[N],ans;
struct Edge {
	int to,nxt;
}e[N<<1];
int hd[N],cnt;
il void ade(int u,int v){
	e[++cnt].to=v,e[cnt].nxt=hd[u],hd[u]=cnt;
}
int ch[N][2],fa[N],siz[N],vs[N]; 
il int Son(int k){
	return ch[fa[k]][1]==k;
}
il bool IsRoot(int k){
	return ch[fa[k]][0]!=k&&ch[fa[k]][1]!=k;
}
il void Pushup(int k){
	siz[k]=siz[ls]+siz[rs]+a[k]+vs[k];
}
il void Rotate(int k){
	int fk=fa[k],gfk=fa[fk];
	int b=Son(k),c=Son(fk),a=ch[k][!b];
	if(!IsRoot(fk))ch[gfk][c]=k;fa[k]=gfk;
	ch[fk][b]=a,fa[a]=fk;
	ch[k][!b]=fk,fa[fk]=k;
	Pushup(fk),Pushup(k);
}
il void Splay(int k){
	while(!IsRoot(k)){
		int fk=fa[k];
		if(!IsRoot(fk)){
			if(Son(k)==Son(fk))Rotate(k);
			else Rotate(fk);
		}
		Rotate(k);
	}
}
il int Calc(int k,int t,int h){
	//先判断子树内垄断颜色,再判断自己 
	return rs?2*(t-h):(2*a[k]>t?2*(t-a[k]):t-1);
}
il void Access(int k,int w){
	Splay(k);
	int sum=siz[k]-siz[ls],mx=siz[rs];
	ans-=Calc(k,sum,mx);
	siz[k]+=w,a[k]+=w,sum+=w;//更新 
	if((mx<<1)<=sum)vs[k]+=mx,rs=mx=0;//垄断节点没了,断实边 
	ans+=Calc(k,sum,mx),Pushup(k);
	int son=k;k=fa[k];
	for(;k;son=k,k=fa[k]){//向上跳虚边 
		Splay(k);
		int sum=siz[k]-siz[ls],mx=siz[rs];//同上 
		ans-=Calc(k,sum,mx);
		siz[k]+=w,vs[k]+=w,sum+=w;//由于k是由虚边而来,所以应该更新vs 
		if((mx<<1)<=sum)vs[k]+=mx,rs=mx=0;
		if((siz[son]<<1)>sum)vs[k]-=(mx=siz[son]),rs=son;//出现垄断节点,连实边 
		ans+=Calc(k,sum,mx),Pushup(k);
	}
}
void DFS(int u,int ff){
	siz[u]=a[u],fa[u]=ff;
	for(rg int i=hd[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v!=ff)DFS(v,u),siz[u]+=siz[v];
	}
	for(rg int i=hd[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v!=ff&&2*siz[v]>siz[u])ch[u][1]=v;
	}
	vs[u]=siz[u]-a[u]-siz[ch[u][1]];
	ans+=Calc(u,siz[u],siz[ch[u][1]]);
}
signed main(){
	Read(n),Read(m);
	for(rg int i=1;i<=n;i++)Read(a[i]);
	for(rg int i=1,u,v;i<n;i++){
		Read(u),Read(v),ade(u,v),ade(v,u);
	}
	DFS(1,0);
	cout<<ans<<endl;
	for(rg int i=1,xi,wi;i<=m;i++){
		Read(xi),Read(wi),Access(xi,wi);
		cout<<ans<<endl;
	}
	return 0;
}

3.Ending

本来想写个 LCT 总结来着,后来发现自己太菜写不出,网上也已经有了不少优质教学,正好自己很久没写单题题解了,于是水了这样一篇博客。

内容来自_ajhfff_的博客(https://www.cnblogs.com/juruoajh/),未经允许,不得转载。
原文地址:https://www.cnblogs.com/juruoajh/p/14490690.html