洛谷P4338/Vijos2043/LOJ2434/UOJ374/BZOJ5212[ZJOI2018]历史(DP+LCT)
给定一棵树,已知每个点的$access$次数,求虚/实边切换次数的最大值。
首先不考虑修改,设一个点$u$修改次数为$a_u$,下属子树的总修改次数为$S_u$,$h_u=\max(a_u,\max{S_v}\text{(v为u的子节点)})$,则可知$2h_u>S_u$时,$u$结点的最大冲突次数为$2(S_u-h_u)$;否则为$S_u-1$。(可以自己试几次,保证理解)
对于$2h_u>S_u$的结点,我们注意到修改它$S_v==h_u$的孩子时答案是不变的,只有修改其它孩子时答案才发生变化,所以我们从u向它连一条实边(不难看出这样的实边最多只有一条),其它孩子连虚边,则可以用$LCT$维护,只有沿虚边向上跳时才需要考虑更新答案和虚实边切换(可能不切换,这点要注意)。
可以发现这种连法具有类似树剖的良好性质,复杂度$O(mlongn)$
#include<cstdio> typedef long long ll; const int N=400050; char rB[1<<21],*S,*T,wB[1<<21]; int wp=-1; inline char gc(){return S==T&&(T=(S=rB)+fread(rB,1,1<<21,stdin),S==T)?EOF:*S++;} inline void flush(){fwrite(wB,1,wp+1,stdout);wp=-1;} inline void pc(char c){if(wp+1==(1<<21))flush();wB[++wp]=c;} inline int rd(){ char c=gc(); while(c<48||c>57)c=gc(); int x=c&15; for(c=gc();c>=48&&c<=57;c=gc())x=(x<<3)+(x<<1)+(c&15); return x; } short buf[25]; inline void wt(ll x){ short l=-1; while(x>9){ buf[++l]=x%10; x/=10; } pc(x|48); while(l>=0)pc(buf[l--]|48); pc('\n'); } int G[N],to[N<<1],nxt[N<<1],sz=0,f[N],ch[N][2]; ll a[N],sum[N],ima[N],ans=0; inline void pushup(int o){sum[o]=a[o]+sum[ch[o][0]]+sum[ch[o][1]]+ima[o];} inline bool dir(int o){return ch[f[o]][1]==o;} inline bool isrt(int o){return ch[f[o]][0]!=o&&ch[f[o]][1]!=o;} inline ll calc(int u,ll s,ll h){return ch[u][1]?(s-h<<1):((a[u]<<1)>s?(s-a[u]<<1):s-1);} inline void rot(int o){ int fa=f[o]; bool d=dir(o); f[o]=f[fa]; if(!isrt(fa))ch[f[fa]][dir(fa)]=o; if(ch[fa][d]=ch[o][d^1])f[ch[fa][d]]=fa; f[ch[o][d^1]=fa]=o; pushup(fa);pushup(o); } inline void splay(int o){ for(;!isrt(o);rot(o))if(!isrt(f[o]))rot((dir(f[o])^dir(o))?o:f[o]); } inline void add(int u,int v){ to[++sz]=v;nxt[sz]=G[u];G[u]=sz; to[++sz]=u;nxt[sz]=G[v];G[v]=sz; } void dfs(int u,int fa){ int i,v,p=0; ll maxn=a[u]; f[u]=fa; sum[u]=a[u]; for(i=G[u];i;i=nxt[i])if((v=to[i])!=fa){ dfs(v,u); sum[u]+=sum[v]; if(sum[v]>maxn)maxn=sum[p=v]; } ans+=(maxn<<1)>sum[u]?(sum[u]-maxn<<1):sum[u]-1; if(p&&(maxn<<1)>sum[u])ch[u][1]=p; ima[u]=sum[u]-sum[ch[u][1]]-a[u]; } inline void update(int x,int w){ int y=x; splay(x); ll s=sum[x]-sum[ch[x][0]],h=sum[ch[x][1]]; ans-=calc(x,s,h); a[x]+=w;sum[x]+=w;s+=w; if((h<<1)<=s){ima[x]+=h;ch[x][1]=0;} ans+=calc(x,s,h); pushup(x); for(x=f[x];x;x=f[y=x]){ splay(x); s=sum[x]-sum[ch[x][0]];h=sum[ch[x][1]]; ans-=calc(x,s,h); sum[x]+=w;ima[x]+=w;s+=w; if((h<<1)<=s){ ima[x]+=h;ch[x][1]=h=0; if((sum[y]<<1)>s){ ima[x]-=(h=sum[y]); ch[x][1]=y; } } ans+=calc(x,s,h); pushup(x); } } int main(){ int n=rd(),m=rd(),i,u,v; for(i=1;i<=n;++i)a[i]=rd(); for(i=1;i<n;++i){ u=rd();v=rd(); add(u,v); } dfs(1,0); wt(ans); while(m--){ u=rd();v=rd(); update(u,v); wt(ans); } flush(); return 0; }
不过出于一些奇怪的原因Vijos上初始化的dfs会爆栈,考虑用bfs求出拓扑序,然后逆着拓扑序处理即可。
#include<cstdio> #include<queue> using namespace std; typedef long long ll; const int N=400050; char rB[1<<21],*S,*T,wB[1<<21]; int wp=-1; inline char gc(){return S==T&&(T=(S=rB)+fread(rB,1,1<<21,stdin),S==T)?EOF:*S++;} inline void flush(){fwrite(wB,1,wp+1,stdout);wp=-1;} inline void pc(char c){if(wp+1==(1<<21))flush();wB[++wp]=c;} inline int rd(){ char c=gc(); while(c<48||c>57)c=gc(); int x=c&15; for(c=gc();c>=48&&c<=57;c=gc())x=(x<<3)+(x<<1)+(c&15); return x; } short buf[25]; inline void wt(ll x){ short l=-1; while(x>9){ buf[++l]=x%10; x/=10; } pc(x|48); while(l>=0)pc(buf[l--]|48); pc('\n'); } int G[N],to[N<<1],nxt[N<<1],sz=0,f[N],ch[N][2],r[N]; ll a[N],sum[N],ima[N],ans=0; queue<int> Q; inline void pushup(int o){sum[o]=a[o]+sum[ch[o][0]]+sum[ch[o][1]]+ima[o];} inline bool dir(int o){return ch[f[o]][1]==o;} inline bool isrt(int o){return ch[f[o]][0]!=o&&ch[f[o]][1]!=o;} inline ll calc(int u,ll s,ll h){return ch[u][1]?(s-h<<1):((a[u]<<1)>s?(s-a[u]<<1):s-1);} //计算冲突值 inline void rot(int o){ int fa=f[o]; bool d=dir(o); f[o]=f[fa]; if(!isrt(fa))ch[f[fa]][dir(fa)]=o; if(ch[fa][d]=ch[o][d^1])f[ch[fa][d]]=fa; f[ch[o][d^1]=fa]=o; pushup(fa);pushup(o); } inline void splay(int o){ for(;!isrt(o);rot(o))if(!isrt(f[o]))rot((dir(f[o])^dir(o))?o:f[o]); } //LCT的Splay部分 inline void add(int u,int v){ to[++sz]=v;nxt[sz]=G[u];G[u]=sz; to[++sz]=u;nxt[sz]=G[v];G[v]=sz; } inline void work(int u){ //处理u int i,v,p=0; ll maxn=a[u]; sum[u]=a[u]; for(i=G[u];i;i=nxt[i])if((v=to[i])!=f[u]){ sum[u]+=sum[v]; if(sum[v]>maxn)maxn=sum[p=v]; } ans+=(maxn<<1)>sum[u]?(sum[u]-maxn<<1):sum[u]-1; if(p&&(maxn<<1)>sum[u])ch[u][1]=p; //计算答案和加边 ima[u]=sum[u]-sum[ch[u][1]]-a[u]; //把u的虚儿子的值储存下来,以完成子树Su大小的计算 } inline void update(int x,int w){ //更新 int y=x; splay(x); ll s=sum[x]-sum[ch[x][0]],h=sum[ch[x][1]]; ans-=calc(x,s,h); a[x]+=w;sum[x]+=w;s+=w; if((h<<1)<=s){ima[x]+=h;ch[x][1]=0;} ans+=calc(x,s,h); pushup(x); for(x=f[x];x;x=f[y=x]){ splay(x); s=sum[x]-sum[ch[x][0]];h=sum[ch[x][1]]; ans-=calc(x,s,h); sum[x]+=w;ima[x]+=w;s+=w; if((h<<1)<=s){ ima[x]+=h;ch[x][1]=h=0; if((sum[y]<<1)>s){ //切换(必须满足新的值可以成为新的实儿子才切换) ima[x]-=(h=sum[y]); ch[x][1]=y; } } ans+=calc(x,s,h); pushup(x); } } int main(){ int n=rd(),m=rd(),i,u,v,tot=1; for(i=1;i<=n;++i)a[i]=rd(); for(i=1;i<n;++i){ u=rd();v=rd(); add(u,v); } Q.push(r[1]=1); while(!Q.empty()){ u=Q.front();Q.pop(); for(i=G[u];i;i=nxt[i])if((v=to[i])!=f[u]){ f[v]=u; r[++tot]=v; Q.push(v); } } //求拓扑序 for(i=n;i;--i)work(r[i]); //倒序处理,保证u的儿子先于u处理 wt(ans); while(m--){ u=rd();v=rd(); update(u,v); wt(ans); } flush(); return 0; }