LCT 学习笔记

引入

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,写这篇博客加深记忆。

作者介绍

新初二的大蒟蒻,以常常垫底和爆零著称。

鸣谢与帮助

标记1:题目来源
标记2:代码来源

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值