[洛谷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 中的一些信息(比如vs
、siz
)和初始答案需要 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 总结来着,后来发现自己太菜写不出,网上也已经有了不少优质教学,正好自己很久没写单题题解了,于是水了这样一篇博客。