引入
LCT是什么?自动细胞学检测系统还是李超线段树?
先看一道题目。
Luogu P3690
1
^1
1
首先,看到树上的路径,我们首先想到的是线段树套重链剖分。但是,连边、删边又是什么鬼?
可能,我们需要一种新的算法,能完成线段树套重链剖分的东西,又能完成连边和删边。
明眼人都知道,底子肯定不可能是重链剖分,难道是长链剖分?
两个都不是,因为我们需要连边删边,一更改不管你是重链还是长链剖分都得完蛋。
看来,只能用实链剖分了。
底子定下来了,那么,用什么来套这个底子呢?线段树、树状数组等低级算法是肯定不行了。或许,二叉平衡树可以?
二叉平衡树里比较秀的也就Treap、Splay和替罪羊树了。我们要选哪一位呢?
替罪羊肯定不行,不平衡了一拍扁,平衡是平衡了,但是结构可就挂了。
Treap呢?想要旋转,更改它堆性质部分的数,也是够麻烦的了。
而Splay变成了我们的首选。随便旋转!
然后,实链剖分加Splay,成为了动态树(Link Cut Tree),简写LCT。
介绍
性质
LCT的性质,一定要记好呦!
1.每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。
2.每个节点包含且仅包含于一个Splay中。
3.边分为实边和虚边,实边包含在Splay中,而虚边总是由一棵Splay指向另一个节点(指向该Splay中中序遍历最靠前的点在原树中的父亲)。
操作
access
access即定义为打通根节点到指定节点的实链,使得一条中序遍历以根开始、以指定点结束的Splay出现。
用人话说就是打通一条实链,使得从根节点到点x不需要通过虚边。
其实步骤很简单,也就四步。
1.x扔到根去
2.x换儿子
3.x更新信息
4.x变为x的轻边所指向的父亲,回到1
什么时候蹦出这个循环?就是x为这个原树的根节点的时候。
看代码 (csdn的代码颜色真好看)
void access(int x) {
for(int y=0; x; y=x,x=f[x]) {
splay(x);
c[x][1]=y;
pushup(x);
}
}
make_root
现在要获取指定两个节点之间的路径信息。
因为路径深度不可能严格递增,所以不可能在一个Splay中(性质1)
-Does that mean we can't do it?
-No, because we have a function called make_root.
make_root定义为换根,让指定点成为原树的根。
这时,需要用到access函数和Splay的各种基本操作。
1.access(x)后x在Splay中一定是深度最大的点
2.splay(x)后,x在Splay中将没有右子树(性质1)。于是翻转整个Splay。
这使得所有点的深度都倒过来了,x没了左子树,反倒成了深度最小的点(根节点)。
是不是很有趣?
代码写出来也很漂亮。
void pushr(int x){//区间翻转
swap(c[x][0],c[x][1]);
r[x]^=1;//r为区间翻转懒标记数组
}
void makeroot(int x){
access(x);splay(x);
pushr(x);
}
find_root
找x所在原树的树根,主要用来判断两点之间的连通性*。
同样利用性质1,不停找左儿子,因为其深度一定比当前点深度小。
int findroot(int x) {
access(x); splay(x);
while(c[x][0]) pushdown(x), x=c[x][0];
//如要获得正确的原树树根,一定pushdown!原因显然。
splay(x);//保证复杂度
return x;
}
*:直接判断原树树根是否相同即可。
split
split(x, y)定义为拉出x−y的路径成为一个Splay
y为该Splay的根
void split(int x, int y) {
make_root(x);
access(y);
splay(y);
}
一旦x成为了根,那么access(y)直接做出x-y。
splay(y)转到根,方便处理以后的东西。
link
link(x, y)是指在x-y连一条轻边,没什么好说的。唯一注意的就是判断合法。
带合法判断版,不带的去掉那句话。
bool link(int x,int y){
makeroot(x);
if(findroot(y)==x)return 0;//两点已经在同一子树中,再连边不合法
f[x]=y;
return 1;
}
cut
实现的东西很简单:将x−y的边断开。
不带合法判断版
使x为根后,y的父亲一定指向x,深度相差一定是1。
当access(y),splay(y)以后,x一定是y的左儿子,直接双向断开连接。
void cut(int x,int y){
split(x,y);
f[x]=c[y][0]=0;
pushup(y);
}
不然呢?
先判一下连通性(注意findroot(y)以后x成了根)
再看看x,y是否有父子关系,还要看y是否有左儿子
为啥这么麻烦?
因为access(y)以后,假如y与x在同一Splay中而没有直接连边,
那么这条路径上就一定会有其它点,在中序遍历序列中的位置会介于x与y之间。
那么可能y的父亲就不是x了。
也可能y的父亲还是x,那么其它的点就在y的左子树中。
三个条件同时满足,才可以断掉。
inline bool cut(int x,int y){
make_root(x);
if(find_root(y)!=x||f[y]!=x||c[y][0]) return 0;
f[y]=c[x][1]=0;//x在findroot(y)后被转到了根
push_up(x);
return 1;
}
如果维护了size,还可以换一种判断
因为access(y) 以后,该Splay只会剩下x,y两个点了(性质1)
inline bool cut(int x,int y){
makeroot(x);
if(findroot(y)!=x||sz[x]>2)return 0;
f[y]=c[x][1]=0;
pushup(x);
return 1;
}
代码
基本操作都讲完了,来看基本操作全家福,就是开头那题的代码。
带注释代码摘自另处
2
^{2}
2
#include<bits/stdc++.h>
#define R register int
#define I inline void
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int SZ=1<<19,N=3e5+9;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
G;while(*ip<'-')G;
R x=*ip&15;G;
while(*ip>'-'){x*=10;x+=*ip&15;G;}
return x;
}
int f[N],c[N][2],v[N],s[N],st[N];
bool r[N];
inline bool nroot(R x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
I pushup(R x){//上传信息
s[x]=s[lc]^s[rc]^v[x];
}
I pushr(R x){R t=lc;lc=rc;rc=t;r[x]^=1;}//翻转操作
I pushdown(R x){//判断并释放懒标记
if(r[x]){
if(lc)pushr(lc);
if(rc)pushr(rc);
r[x]=0;
}
}
I rotate(R x){//一次旋转
R y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;//额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
if(w)f[w]=y;f[y]=x;f[x]=z;
pushup(y);
}
I splay(R x){//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
R y=x,z=0;
st[++z]=y;//st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
while(nroot(y))st[++z]=y=f[y];
while(z)pushdown(st[z--]);
while(nroot(x)){
y=f[x];z=f[y];
if(nroot(y))
rotate((c[y][0]==x)^(c[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
I access(R x){//访问
for(R y=0;x;x=f[y=x])
splay(x),rc=y,pushup(x);
}
I makeroot(R x){//换根
access(x);splay(x);
pushr(x);
}
int findroot(R x){//找根(在真实的树中的)
access(x);splay(x);
while(lc)pushdown(x),x=lc;
splay(x);
return x;
}
I split(R x,R y){//提取路径
makeroot(x);
access(y);splay(y);
}
I link(R x,R y){//连边
makeroot(x);
if(findroot(y)!=x)f[x]=y;
}
I cut(R x,R y){//断边
makeroot(x);
if(findroot(y)==x&&f[y]==x&&!c[y][0]){
f[y]=c[x][1]=0;
pushup(x);
}
}
int main()
{
R n=in(),m=in();
for(R i=1;i<=n;++i)v[i]=in();
while(m--){
R type=in(),x=in(),y=in();
switch(type){
case 0:split(x,y);printf("%d\n",s[y]);break;
case 1:link(x,y);break;
case 2:cut(x,y);break;
case 3:splay(x);v[x]=y;//先把x转上去再改,不然会影响Splay信息的正确性
}
}
return 0;
}
我的代码(p.s.没注释,差不多)
#include<cstdio>
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int SZH=1<<9, N=3e5+10;
char ch[SZH];
int f[N],c[N][2],v[N],s[N],st[N], n, m;
bool r[N];
bool nroot(int x){
return c[f[x]][0]==x||c[f[x]][1]==x;
}
void pushup(int x){
s[x]=s[lc]^s[rc]^v[x];
}
void pushr(int x){
int t=lc;lc=rc;rc=t;r[x]^=1;
}
void pushdown(int x){
if(r[x]){
if(lc)pushr(lc);
if(rc)pushr(rc);
r[x]=0;
}
}
void rotate(int x){
int y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;
if(w)f[w]=y;f[y]=x;f[x]=z;
pushup(y);
}
void splay(int x){
int y=x,z=0;
st[++z]=y;
while(nroot(y))st[++z]=y=f[y];
while(z)pushdown(st[z--]);
while(nroot(x)){
y=f[x];z=f[y];
if(nroot(y))
rotate((c[y][0]==x)^(c[z][0]==y)?x:y);
rotate(x);
}
pushup(x);
}
void access(int x){
for(int y=0;x;x=f[y=x])
splay(x),rc=y,pushup(x);
}
void makeroot(int x){
access(x);splay(x);
pushr(x);
}
int findroot(int x){
access(x);splay(x);
while(lc)pushdown(x),x=lc;
splay(x);
return x;
}
void split(int x, int y){
makeroot(x);
access(y);splay(y);
}
void link(int x, int y){
makeroot(x);
if(findroot(y)!=x)f[x]=y;
}
void cut(int x, int y){
makeroot(x);
if(findroot(y)==x&&f[y]==x&&!c[y][0]){
f[y]=c[x][1]=0;
pushup(x);
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++) scanf("%d", &v[i]);
for(int i=1; i<=m; i++) {
int flag, x, y;
scanf("%d%d%d", &flag, &x, &y);
if(!flag) {
split(x, y);
printf("%d\n", s[y]);
} else if(flag==1) {
link(x, y);
} else if(flag==2) {
cut(x, y);
} else {
splay(x);
v[x]=y;
}
}
}
后记
LCT可以看作是动态版本的线段树套重链剖分。Link Cut Tree的名字就是来自与静剖最明显的区别,支持Link和Cut操作。其实动剖完全可以做静剖的题目,时间复杂度少一个 l o g log log,而且常数也不一定大,有时动剖的实现还比静剖简单。当然动剖的题目静剖不一定能做。静剖和动剖还是需要结合情况使用。
学习笔记背景
之前几个算法学得懵懵懂懂,今天必须要学好LCT,写这篇博客加深记忆。
作者介绍
新初二的大蒟蒻,以常常垫底和爆零著称。