upd on 2021.10.17:终于来填坑了,我怕不是一个大鸽子(
LCT,全称 Link Cut Tree,又称 LiChaoTree(bushi),是一种支持动态维护森林的数据结构,支持动态加边、加边、查询路径信息,有时也能支持查询子树信息,但是注意,这里的图时时刻刻必须是一棵森林,也就是说如果加入的边的两个端点本来就在一个连通块中,那我们是无法加入这条边的,因此 LCT 无法直接解决动态图连通性问题,需要配合 ETT 等高级技巧才能解决,由于蒟蒻对这种高端科技一窍不通所以就不在这里瞎 BB 了。
LCT 内部结构是若干棵 Splay 组成的森林。更具体地说,我们考虑选取一个点为根进行一遍 DFS,然后我们将原图划分成若干个祖先到后代的链(这个划分方式会随图的不断变化而发生变化),然后每条链构造一个 Splay,具体构造方式如下:
- 对于一条链,我们将其中的点以按深度大小为关键字建立一棵 BST,换句话说每个 Splay 中序遍历就是这条链中的点按深度大小从小到大排序后得到的序列。
- 一条链对应的 Splay 的父亲,是这条链链顶节点在原树上的父亲,但这个父亲却不认这个儿子,如果我们把链上的边称作实边,不在链上的边称为虚边,那么有实边认父也认子,虚边认父不认子。
譬如有如下图所示的树,我们对其进行如下方式的链划分(同一种颜色的边表示同一条链,灰边不在任何一条链上):
那么一种可能的 splay 构造方式如下:
那么如何实现 LCT 呢?首先我们先来看看 LCT 一些基本的结构体的定义:
struct node{int val,sum,ch[2],f;} s[MAXN+5];
void pushup(int k){
s[k].sum=s[k].val;
if(s[k].ch[0]) s[k].sum^=s[s[k].ch[0]].sum;
if(s[k].ch[1]) s[k].sum^=s[s[k].ch[1]].sum;
}
int ident(int k){return ((s[s[k].f].ch[0]==k)?0:((s[s[k].f].ch[1]==k)?1:-1));}
void connect(int k,int f,int op){s[k].f=f;if(~op) s[f].ch[op]=k;}
void rotate(int x){
int y=s[x].f,z=s[y].f,dx=ident(x),dy=ident(y);
connect(s[x].ch[dx^1],y,dx);connect(y,x,dx^1);connect(x,z,dy);
pushup(y);pushup(x);
}
其中 (sum) 表示你 LCT 中需要维护的子树信息,对应到原树上就是一条路径上的信息,( ext{ident}, ext{connect}, ext{rotate}) 都和普通 Splay 的写法大同小异。值得注意的是,如果 (k) 本身就是其所在 Splay 的根节点,那么 ( ext{ident}(k)) 会返回 (-1),此时 ( ext{connect}, ext{rotate}) 写法也应有相应的改变。
接下来是 splay
,由于在 LCT 中,splay
函数用途单一:将一个点旋到对应 Splay 的根,因此只用传一个参数就够了。
void splay(int k){
while(~ident(k)){
if(ident(s[k].f)==-1) rotate(k);
else if(ident(k)==ident(s[k].f)) rotate(s[k].f),rotate(k);
else rotate(k),rotate(k);
}
}
LCT 的基础操作都讲完了,接下来是一些 LCT 特有的操作:
access 函数
我们定义函数 ( ext{access}(k)) 表示:将 (k) 到根节点路径上的边打包成一个 splay
(当然执行完 access(k)
后,这条路径对应的 splay 的根不一定是 (k),因此常常 access 之后要执行 splay
才能将 (k) 转到根的位置)并将所有与这条路径上的点相连的所有重边自动设为轻边。
考虑如何实现这一操作。首先我们将 (k) 旋到其所在 Splay 的根,那么我们要将 (k) 与其在链上下方的点之间的边设为轻边——这个其实挺好实现,我们直接将此时节点 (k) 的右儿子设为 (0) 就行了,这样轻边认父不认子,原来 (k) 的右子树单独成一个 Splay,认 (k) 作这个父亲,但是 (k) 不认原本的右子树做这个儿子,也就达成了我们想要的效果。而对于 (k) 的父亲到原本这条链的链顶节点这条路径上的节点,由于原本与它们相连的轻边现在还是轻边,因此我们不用对它们的子树信息做任何变化。注意时刻更新信息,儿子变了,信息也要随时上推。
向上推,我们继续跳到 (1 o k) 路径上的倒数第二条链——根据 LCT 的构造,这条链的上一层节点肯定是此时 (k) 在 LCT 上的父亲,我们称之为 (k’),那么我们将 (k’) 转到根,那么我们要将 (k) 所在的链接在 (k’) 所在的链的下方,同时将原本 (k’) 下方的节点与 (k’) 之间的边设为轻边,这个同样可以用一步非常简单的操作描述:将 (k’) 的右儿子设为之前的 (k),因为这样相当于将之前 (k) 对应的 Splay 接到 (k’) 的右子树处设为 (k’) 这条链中,比 (k’) 深度更大的节点,而原本 (k’) 右子树,则因为认父不认子变成了虚边,自成一条链。
倘若我们再往上跳一条链,那么过程也是类似的:设 (k’) 父亲为 (k''),那么我们将 (k’') 转到根,将其右子树设为 (k’),上推信息,如此操作下去直到跳到原树的根节点即可。
’因此 access 函数的操作可以简单描述为以下四步:
- 转到根
- 换儿子
- pushup
- 跳父亲
void access(int k){
int pre=0;
for(;k;pre=k,k=s[k].f) splay(k),s[k].ch[1]=pre,pushup(k);
}
makeroot 操作
顾名思义,makeroot(k)
函数的效果就是将 (k) 设为原树的根。
我们打通 (k) 到原本树根之间的链,并将 (k) 转到根。那么画个图可以发现,除了 (k) 到原本树根这条路径上的点之间路径上所有点的相对深度会发生变化(整体颠倒过来)之外,其余所有链的相对深度都不会发生任何变化,因此我们只用将 (k) 到原本树根这条链上的点的中序遍历 reverse
一下即可。这个可以通过打一个子树翻转的标记实现,具体来说我们在结构体中定义一个标记 lz
,那么我们新增一个 pushdown
函数,如下所示:
void tag(int k){swap(s[k].ch[0],s[k].ch[1]);s[k].lz^=1;}
void pushdown(int k){
if(s[k].lz){
if(s[k].ch[0]) tag(s[k].ch[0]);
if(s[k].ch[1]) tag(s[k].ch[1]);
s[k].lz=0;
}
}
清晰明了。
当然有了这个标记之后,splay
函数的过程也应做出相应变化。由于 (k) 到根这条路径上可能会有标记没有下推,因此我们要先一遍 DFS 依次下推根节点到 (k) 路径上所有节点的标记,这个可以通过一个函数 pushall
实现:
void pushtag(int k){if(~ident(k)) pushtag(s[k].f);pushdown(k);}
这样我们需要在 splay
函数前写上:
void splay(int k){
pushtag(k);//加注释部分为新增的部分
while(~ident(k)){
if(ident(s[k].f)==-1) rotate(k);
else if(ident(k)==ident(s[k].f)) rotate(s[k].f),rotate(k);
else rotate(k),rotate(k);
}
}
有了 lz
标记作为铺垫以后,makeroot
函数实现起来就特别容易了:
void makeroot(int k){access(k);splay(k);tag(k);}
表示打通 (k) 到根节点的路径,然后将 (k) 转到根,最后在 (k) 处打一个整体翻转的标记。
findroot 函数
顾名思义,findroot(k)
函数返回 (k) 所在连通块的根。
按照套路,我们还是打通 (k) 到根节点之间的路径,那么显然 (k) 所在连通块的根就是这个 Splay 中中序遍历在第一位的节点,直接每次进入左子树即可。注意时时 pushdown
,注意找到根之后就把根旋到 Splay 的根处,否则复杂度会退化!!!
int findroot(int k){
access(k);splay(k);
while(s[k].ch[0]) pushdown(k),k=s[k].ch[0];
splay(k);return k;
}
split 函数
有了神奇的 access
与 makeroot
函数之后,我们就可以单独将两点间的路径提取出来形成一个 Splay 了。
具体方法就是先将 (x) 设为根,再打通 (y) 到根的路径,再将 (y) 转到 Splay 的根。此时通过调用 (y) 的信息即可知道 (x o y) 路径上的信息。
注意 (x,y) 必须连通!否则该操作无效。
void split(int x,int y){makeroot(x);access(y);splay(y);}
link 函数
定义函数 link(x,y)
表示在原树中连上 (x,y) 之间的边,如果 (x,y) 已经在一个连通块中则不连。
我们先执行 makeroot(x)
,然后如果 findroot(y)=x
则说明 (x,y) 已经连通,直接返回,否则我们直接将 (x) 的父亲设为 (y)。
void link(int x,int y){makeroot(x);if(findroot(y)!=x) s[x].f=y;}
cut 函数
定义函数 link(x,y)
表示在割掉原树中 (x,y) 之间的边,如果 (x,y) 之间没有边则不做任何操作。
如果保证边 ((x,y)) 存在那倒好办,直接提取出 (x,y) 之间的路径,那么这条路径的 splay 大小一定为 (2) 并且根节点为 (y),直接将 (y) 的左儿子与 (x) 的父亲均设为 (0),即双向断开这条边即可。
那如果不保证 ((x,y)) 边存在怎么办呢?首先先 makeroot(x)
,如果 (y) 所在连通块的根 (
e x) 则 (x,y) 肯定不在一个连通块中,否则由于我们执行了 findroot
操作,所以相当于自动提取出了 (x,y) 之间的路径,其中 (x) 为根。我们只用判断这个 Splay 大小是否为 (2) 即可。如果我们维护了子树 (siz) 那可以通过查询 (x) 的 (siz) 是否为 (2) 判断,否则我们需要检验 (x,y) 之间是否有别的点,可以发现,如果 (y) 的父亲不是 (x),或者 (y) 的左子树非空,那么 (x,y) 在中序遍历上不相邻,否则 (x,y) 之间一定存在边相连,双向断开即可。
void cut(int x,int y){
makeroot(x);
if(findroot(y)!=x) return;
if(s[y].f!=x||s[y].ch[0]) return;
s[y].f=s[x].ch[1]=0;pushup(x);
}
LCT 的时间复杂度
LCT 复杂度为均摊 (Theta(nlog n)),证明大概就可以把 LCT 看作若干个 Splay,那么其总势能就是全部 Splay 的势能相加,因此其复杂度同 Splay,为 (nlog n)。
模板题代码如下:
const int MAXN=1e5;
int n,qu;
struct node{int val,sum,ch[2],lz,f;} s[MAXN+5];
void pushup(int k){
s[k].sum=s[k].val;
if(s[k].ch[0]) s[k].sum^=s[s[k].ch[0]].sum;
if(s[k].ch[1]) s[k].sum^=s[s[k].ch[1]].sum;
}
int ident(int k){return ((s[s[k].f].ch[0]==k)?0:((s[s[k].f].ch[1]==k)?1:-1));}
void tag(int k){swap(s[k].ch[0],s[k].ch[1]);s[k].lz^=1;}
void pushdown(int k){
if(s[k].lz){
if(s[k].ch[0]) tag(s[k].ch[0]);
if(s[k].ch[1]) tag(s[k].ch[1]);
s[k].lz=0;
}
}
void connect(int k,int f,int op){s[k].f=f;if(~op) s[f].ch[op]=k;}
void rotate(int x){
int y=s[x].f,z=s[y].f,dx=ident(x),dy=ident(y);
connect(s[x].ch[dx^1],y,dx);connect(y,x,dx^1);connect(x,z,dy);
pushup(y);pushup(x);
}
void pushtag(int k){if(~ident(k)) pushtag(s[k].f);pushdown(k);}
void splay(int k){
pushtag(k);
while(~ident(k)){
if(ident(s[k].f)==-1) rotate(k);
else if(ident(k)==ident(s[k].f)) rotate(s[k].f),rotate(k);
else rotate(k),rotate(k);
}
}
void access(int k){
int pre=0;
for(;k;k=s[k].f) splay(k),s[k].ch[1]=pre,pre=k,pushup(k);
}
void makeroot(int k){access(k);splay(k);tag(k);}
void split(int x,int y){makeroot(x);access(y);splay(y);}
int findroot(int k){
access(k);splay(k);
while(s[k].ch[0]) pushdown(k),k=s[k].ch[0];
splay(k);return k;
}
void link(int x,int y){makeroot(x);if(findroot(y)!=x) s[x].f=y;}
void cut(int x,int y){
makeroot(x);
if(findroot(y)!=x) return;
if(s[y].f!=x||s[y].ch[0]) return;
s[y].f=s[x].ch[1]=0;pushup(x);
}
int main(){
scanf("%d%d",&n,&qu);
for(int i=1;i<=n;i++) scanf("%d",&s[i].val),s[i].sum=s[i].val;
while(qu--){
int opt;scanf("%d",&opt);
if(opt==0){
int x,y;scanf("%d%d",&x,&y);
split(x,y);printf("%d
",s[y].sum);
} else if(opt==1){
int x,y;scanf("%d%d",&x,&y);
link(x,y);
} else if(opt==2){
int x,y;scanf("%d%d",&x,&y);
cut(x,y);
} else {
int x,v;scanf("%d%d",&x,&v);
makeroot(x);s[x].val=v;pushup(x);
}
}
return 0;
}