树链剖分可以解决很多与链有关的问题。可是,如果我们维护的树有变化,树链剖分便黯然失色。这时候,LCT就派上用场了。LCT是一种神奇的数据结构。它的主要思路就是用多颗splay来维护一棵树的若干子树。(或者维护森林)
下面从几个方面介绍一下LCT:
1.轻边与重边
与树链剖分类似,LCT也把边分为轻边和重边。而是否是重边,是根据splay中的父亲确定的。每一颗splay中,对于一个节点,它的儿子节点与它相连的边就是重边。因此,重边是双向的——即既可以从儿子找父亲,也可以从父亲找儿子。而轻边则可以理解为单向的,只可以通过儿子的fa数组找到父亲,而父亲处却不包含这个儿子的信息。显而易见,对于一个重边构成的联通块,就是一颗splay所维护的子树。
2.splay与原图的关系
事实上,由于splay的伸展操作,我们维护的树的样子可能不同于原树的样子。因此,我们写代码时必须明白splay的中序遍历是它所维护的子树的中序遍历。fa(父亲数组),ch(儿子数组)都记录的是splay中的父子关系,而每一棵splay根的父亲代表了当前splay联通块与其“父联通块”所连接的的边,但是必须注意,splay根的父亲代表的是联通块之间的关系,所以原树中可能并不存在这条边。
因此,我们用如下代码判断splay的根。
inline bool isroot(int x)
{
return !fa[x]||(x!=ch[fa[x]][0]&&x!=ch[fa[x]][1]);
}
3.access
这个操作的主要目的是将一个节点到根的路径上的边全部变为重边。这个操作的实现很简单,每到达一个splay联通块不断向父联通块走,路上不断将经过的splay联通块之间的边变为重边,直到走到根。
inline void access(int x)
{
for(int re y=0;x;y=x,x=fa[x])
splay(x),ch[x][1]=y,pushup(x);
}
由于splay的个数比较稳定,因此这个操作的复杂度是可以得到保证的。这样以后,当前节点就与根在同一颗splay中,而路径信息也就相应地保存在了这一颗splay中,方便询问等操作。
4.makeroot(换根操作)
需要注意的是,这个换根是在换原树的根,而不是splay的根。实现很简单,把当前节点和根连接起来,接着splay当前节点到根,然后交换左右儿子,打上翻转标记(这个有点类似文艺平衡树的区间翻转,这里相当于翻转中根序达到换根的目的)。
inline void pushnow(int u)
{
laz[u]^=1;
swap(ch[u][1],ch[u][0]);
}
inline void makert(int x)
{
access(x);
splay(x);
pushnow(x);
}
5.splay(伸展操作)
需要注意的是,这个是在换splay的根。与通的splay不同的是,我们一般是从儿子找父亲,而普通的splay是从父亲找儿子。因此,我们必须先找到当前splay的根,以防止标记没有下传。
inline void rotate(int x)
{
int y=fa[x],z=fa[y],d=get(x);
if(!isroot(y))ch[z][get(y)]=x;
fa[x]=z;fa[y]=x;ch[y][d]=ch[x][d^1];
ch[x][d^1]=y;fa[ch[y][d]]=y;
pushup(y),pushup(x);
}
inline void splay(int u)
{
top=0;
s[++top]=u;
for(int re i=u;!isroot(i);i=fa[i])s[++top]=fa[i];
while(top)pushdown(s[top--]);
for(int re y=fa[u];!isroot(u);rotate(u),y=fa[u])
if(!isroot(y))rotate(get(u)==get(y)?y:u);
}
6.split(将两个节点x,y之间的路径变为重路径)
这个操作主要是为了链查询和链修改。实现很简单,把x变为原树的根,接着access(y),此时便将x,y的路径变为重路径,我们再把y splay到根,这样y就拥有了整条路径的信息。
inline void split(int x,int y)
{
makert(x);access(y);splay(y);
}
7.find
这个操作是为了找到某个节点所在树的根节点。(原树中的根节点)(一般维护森林时需要用find判断连通性)
inline int find(int u)
{
access(u);splay(u);
while(ch[u][0])pushdown(u),u=ch[u][0];
splay(u);return u;
}
8.link,cut
都属于比较简单的操作,目的是连边和删边。需要注意的是,link里面连接的是轻边,不需要pushup,由于有些链修改的题需要打标记,如果pushup可能会导致当前节点标记未下传而pushup,如果儿子信息尚未更新,会使当前节点的信息变回原来修改之前的信息。最好在pushup里加pushdown处理这个问题。
inline void link(int x,int y)
{
makert(x);fa[x]=y;
}
inline void cut(int x,int y)
{
if(find(x)!=find(y))return;//如果两者之间没有边,返回
split(x,y);
if(fa[x]!=y || ch[x][1])return;//如果两者之间没有边或者两者之间还有其它节点,返回
ch[y][0]=fa[x]=0;pushup(y);
}
【例题】【模板】LCT(动态树)
【题目描述】
给定n个点以及每个点的权值,要你处理接下来的m个操作。操作有4种。操作从0到3编号。点从1到n编号。
0:后接两个整数(x,y),代表询问从x到y的路径上的点的权值的xor和。保证x到y是联通的。
1:后接两个整数(x,y),代表连接x到y,若x到y已经联通则无需连接。
2:后接两个整数(x,y),代表删除边(x,y),不保证边(x,y)存在。
3:后接两个整数(x,y),代表将点x上的权值变成y。
【输入】
第1行两个整数,分别为n和m,代表点数和操作数。
第2行到第n+1行,每行一个整数,整数在[1,10^9]内,代表每个点的权值。
第n+2行到第n+m+1行,每行三个整数,分别代表操作类型和操作所需的量。
【输出】
对于每一个0号操作,你须输出x到y的路径上点权的xor和。
样例输入
3 3
1
2
3
1 1 2
0 1 2
0 1 1
样例输出
3
1
完整代码:
#include<bits/stdc++.h>
#define re register
using namespace std;
int n,m,a,b,c;
inline int red()
{
int data=0;int w=1; char ch=0;
ch=getchar();
while(ch!='-' && (ch<'0' || ch>'9')) ch=getchar();
if(ch=='-') w=-1,ch=getchar();
while(ch>='0' && ch<='9') data=(data<<3)+(data<<1)+ch-'0',ch=getchar();
return data*w;
}
const int N=3e5+5;
struct lct{
int ch[N][2],fa[N],sum[N],val[N],laz[N];
int top,s[N];
int siz[N];
inline void pushnow(int u)
{
laz[u]^=1;
swap(ch[u][1],ch[u][0]);
}
inline int get(int x){return x==ch[fa[x]][1];}
inline void pushdown(int u)
{
if(!laz[u])return;
pushnow(ch[u][1]);
pushnow(ch[u][0]);
laz[u]=0;
}
inline void pushup(int u)
{
sum[u]=sum[ch[u][1]]^sum[ch[u][0]]^val[u];
siz[u]=siz[ch[u][1]]+siz[ch[u][0]]+1;
}
inline bool isroot(int x){return !fa[x]||(x!=ch[fa[x]][0]&&x!=ch[fa[x]][1]);}
inline void rotate(int x)
{
int y=fa[x],z=fa[y],d=get(x);
if(!isroot(y))ch[z][get(y)]=x;
fa[x]=z;fa[y]=x;ch[y][d]=ch[x][d^1];
ch[x][d^1]=y;fa[ch[y][d]]=y;
pushup(y),pushup(x);
}
inline void splay(int u)
{
top=0;
s[++top]=u;
for(int re i=u;!isroot(i);i=fa[i])s[++top]=fa[i];
while(top)pushdown(s[top--]);
for(int re y=fa[u];!isroot(u);rotate(u),y=fa[u])
if(!isroot(y))rotate(get(u)==get(y)?y:u);
}
inline void access(int x)
{
for(int re y=0;x;y=x,x=fa[x])
splay(x),ch[x][1]=y,pushup(x);
}
inline void makert(int x){access(x);splay(x);pushnow(x);}
inline int find(int u)
{
access(u);splay(u);
while(ch[u][0])pushdown(u),u=ch[u][0];
splay(u);return u;
}
inline void split(int x,int y)
{
makert(x);access(y);splay(y);
}
inline void link(int x,int y)
{
makert(x);fa[x]=y;
}
inline void cut(int x,int y)
{
if(find(x)!=find(y))return;
split(x,y);
if(fa[x]!=y || ch[x][1])return;
ch[y][0]=fa[x]=0;pushup(y);
}
inline int query(int x,int y)
{
split(x,y);return sum[y];
}
}lct;
int main()
{
scanf("%d%d",&n,&m);
for(int re i=1;i<=n;i++)lct.val[i]=red();
while(m--)
{
int opt=red(),x=red(),y=red();
if(opt==0)
printf("%d\n",lct.query(x,y));
else if(opt==1)
{
if(lct.find(x)!=lct.find(y))
lct.link(x,y);
}
else if(opt==2)lct.cut(x,y);
else
{
lct.access(x);lct.splay(x);lct.val[x]=y;lct.pushup(x);
}
}
}