链剖&LCT总结
在搞LCT之前,我们不妨再看看喜闻乐见的树链剖分。
树链剖分有一道喜闻乐见的例题:NOI2015 软件包管理器
如果你看懂题目了,你就会明白它是叫你维护一个树,这棵树是不会动的,要兹磁子树求和,子树修改,树上路径求和,树上路径修改。
树链剖分就是把一个树剖分成像这样的东西:
一棵树用一坨重链组成,重链之间用轻链连接。
对于树上的每一个点,它和子树大小最大的那个的根节点在同一重链,其他儿子另成一条新重链。
这样可以证明每个点到根至多只有log级这么多段的连续的重链。
然后我们把连续的一坨重链用线段树维护一下。
代码(常数非常大
#include <iostream> #include <stdio.h> #include <stdlib.h> using namespace std; #define SIZ 266666 namespace segt { #define LC(x) ((x)<<1) #define RC(x) (LC(x)+1) int M=131072,ls[SIZ],rs[SIZ],tag[SIZ],sum[SIZ]; inline bool avb(int x) { return 1<=x&&x<=2*M; } void pd(int x) { if(!avb(x)||tag[x]==-1) return; if(avb(LC(x))) tag[LC(x)]=tag[x]; if(avb(RC(x))) tag[RC(x)]=tag[x]; sum[x]=(rs[x]-ls[x]+1)*tag[x]; tag[x]=-1; } void upd(int x) { pd(LC(x)); pd(RC(x)); sum[x]=0; if(avb(LC(x))) sum[x]+=sum[LC(x)]; if(avb(RC(x))) sum[x]+=sum[RC(x)]; } int query(int cur,int l,int r) { if(!avb(cur)||l>r) return 0; pd(cur); if(ls[cur]==l&&rs[cur]==r) return sum[cur]; int mid=(ls[cur]+rs[cur])>>1; int ans=query(LC(cur),l,min(mid,r))+query(RC(cur),max(mid+1,l),r); upd(cur); return ans; } void edit(int cur,int l,int r,int x) { if(!avb(cur)||l>r) return; pd(cur); if(ls[cur]==l&&rs[cur]==r) {tag[cur]=x; return;} int mid=(ls[cur]+rs[cur])>>1; edit(LC(cur),l,min(mid,r),x); edit(RC(cur),max(mid+1,l),r,x); upd(cur); } void init() { for(int i=1;i<=M;i++) sum[i+M]=0, ls[i+M]=rs[i+M]=i, tag[i+M]=-1; for(int i=M-1;i>=1;i--) { tag[i]=-1; sum[i]=sum[LC(i)]+sum[RC(i)]; ls[i]=ls[LC(i)]; rs[i]=rs[RC(i)]; } } } namespace lct //然而只是链剖 { int n,S=0,ns[SIZ],fs[SIZ],ss[SIZ], fa[SIZ],siz[SIZ],ws[SIZ],dep[SIZ], fe[SIZ],top[SIZ],X=0,ls[SIZ]; void setc(int s,int f) //setchild { fa[s]=f; ++S; ns[S]=fs[f]; fs[f]=S; ss[S]=s; } void dfs1(int cur) { siz[cur]=1; ws[cur]=0; int csc=-233; for(int x=fs[cur];x;x=ns[x]) { int c=ss[x]; fa[c]=cur; dep[c]=dep[cur]+1; dfs1(c); if(siz[c]>csc) csc=siz[c], ws[cur]=c; siz[cur]+=siz[c]; } } void dfs2(int cur,int tp) { fe[cur]=++X; top[cur]=tp; if(ws[cur]) dfs2(ws[cur],tp); for(int x=fs[cur];x;x=ns[x]) { int c=ss[x]; if(c!=ws[cur]) dfs2(c,c); } ls[cur]=X; } void s2(int cur) { printf("%d\n",segt::query(1,fe[cur],ls[cur])); segt::edit(1,fe[cur],ls[cur],0); } void s1(int x) { int u=1,v=x,ds=dep[v]-dep[u]+1,sum=0; int f1=top[u],f2=top[v]; while(f1!=f2) { if(dep[f1]<dep[f2]) swap(f1,f2), swap(u,v); //u is deeper... sum+=segt::query(1,fe[f1],fe[u]); segt::edit(1,fe[f1],fe[u],1); u=fa[f1]; f1=top[u]; } if(dep[u]>dep[v]) swap(u,v); sum+=segt::query(1,fe[u],fe[v]); segt::edit(1,fe[u],fe[v],1); printf("%d\n",ds-sum); } } int main() { int n,tmp; scanf("%d",&n); for(int i=2;i<=n;i++) { scanf("%d",&tmp); lct::setc(i,tmp+1); } lct::dfs1(1); lct::dfs2(1,1); segt::init(); int Q; scanf("%d",&Q); char iu[20]; int x; while(Q--) { scanf("%s%d",iu,&x); ++x; if(iu[0]=='i') lct::s1(x); else lct::s2(x); } }
稍微做一点说明吧。
dfs1这个函数就是基本的一些处理,dfs2这个函数是用来分配轻重链的。
然后子树操作就只要把这个子树里面的所有重链在线段树里面修改就行。
链的代码:
void s1(int x) { int u=1,v=x,ds=dep[v]-dep[u]+1,sum=0; int f1=top[u],f2=top[v]; while(f1!=f2) { if(dep[f1]<dep[f2]) swap(f1,f2), swap(u,v); //u is deeper... sum+=segt::query(1,fe[f1],fe[u]); segt::edit(1,fe[f1],fe[u],1); u=fa[f1]; f1=top[u]; } if(dep[u]>dep[v]) swap(u,v); sum+=segt::query(1,fe[u],fe[v]); segt::edit(1,fe[u],fe[v],1); printf("%d\n",ds-sum); }
在u和v不在同一条重链上时,把u和v所在重链头(就是同一条重链上深度最小的)中深度大的那个往上走,顺路更新线段树。
当u和v在同一条重链上时直接修改线段树就可以了。此时深度比较小的那个就是lca。(你想这样求lca的话我也没意见
那如果是不是边权而是点权的话就把父向边的边权设为点权就行,路经询问时记得要统计一下lca的点权,同样子树询问的时候要统计一下根节点的点权。
因为每个点到根的路径上至多有log级的重链,像这样搞的复杂度大概是O(logn*数据结构)。如果你用线段树来维护那就是O(log^2n)。但是实际复杂度往往到不了这个级别…
LCT和链剖也是类似的,也是把一个树(或森林,下文为了方便就直接说是树了)剖成若干条链,但是这里的重链和轻链有区别,每访问一个点,我们就把它到根的路径全变成重链。这个访问操作一般叫做“access”。
在LCT中我们把这个“重链”叫做Preferred Edge,把一段不能再延伸的重链叫做Preffered Path,如果结点v的子树中,最后被访问的结点在子树w中,这里w是v的儿子,那么就称w是v的Preferred Child,如果最后被访问过的结点就是v本身,那么它没有Preferred Child。
access操作看起来像这样:
LCT主体用splay维护,和链剖一样,splay里面的一条重链是存在一棵子树里的。但是这颗splay的father比较特殊…如果一个点它是这条重链splay里面最上面的一个点,那么它的父亲就是树上实际的父亲,否则就是正常splay的父亲。
剩下的事情呢如果泥会splay就比较trivial了。你要access一个点,你就把它旋到这条重链的顶上,此时它的父亲就是树上的父亲了对吧。然后把右子树接到这条重链上面的下一个点。然后把“下一个点”设为这个点,再往父亲走。
那么为什么要接右子树呢?因为我们希望中序遍历的时候是连续的一条重链…这个随意理解一下,至于右子树怎么办……爱怎么办怎么办,因为右子树的父亲不会变啊,而且右子树对应的也就是链上深度低一点的某一个点,所以并没有什么事情。
接下来还有一个基本操作叫makeroot(x),这个操作意思是让x成为整棵树的根。我们先access(x)(使x到根节点为一条重链),然后splay(x)(使x成为这条重链最上面的一个点),然后再把x这棵子树打一个翻转标记就行了。
为什么这样是可行的呢?因为x这个重链以外的东西跟根是什么并没有什么关系,只要把x这条重链翻转过来x就会在最顶上了。
然后findroot(x),找到x所在子树在原树上的根节点(因为makeroot可能会改变根节点)。这个最简单,先access(x),然后splay(x),然后一直往左孩子找,找到最左边splay一下然后返回就行。(为什么要splay回去?似乎不splay也行…复杂度玄学
然后cut(a,b),把树中a和b的连边切断,搞成两棵新树。先makeroot(a),然后access(b),然后splay(b),最后把b的左孩子和a的父亲都设为0。
还有link(a,b),把a和b中间连一条边。先makeroot(a),然后fa[a]不是为空吗,就fa[a]=b,最后在splay(a) (又是玄学
这样LCT就嘴巴写完啦。代码稍后放下一篇文章放