LCT(link-cut-tree)入门

ps:由于博主的qwq,把很多实边写成了实链,大家就将就着看吧嘿嘿。

LCT,全称动态树,用于应付不断地加边删边的树的一种算法,非常非常优秀。

因为其经典操作link和cut而被叫做link cut tree(是吗??猜的)

emm  link:链接,cut:砍掉(滑稽)

现在我们开始。(先声明LCT维护的是一个森林哈!)

引入:仨链剖分

我们一般提到的三种剖分是:重链剖分,实链剖分,和长链剖分。

前面我们已经讲过的树链剖分实际上就是重链剖分的别称。长链剖分不太常用(但是sslz_fsy非常强三个都会orz),而本次LCT用到的就是实链剖分。

实链剖分

这个不需要特别学习,你只需要知道这种剖分的优点在于:可以调整,即可以变化。(不像重链剖分那样是固定的)

同样,每个节点对儿子只有一个实链,其它都是虚链。这样做的原因是方便splay来维护。(后话)

它的体现是:所有子节点都有father,但father的son只有实链链接的子节点或者原树上的父节点。意思是说你从叶子节点可以一直跳跳跳跳到根节点,每个叶子节点都可以,但对于一个根节点往下走,它的路径是唯一的。

接下来我们说一下,splay在树里面的运转方式。

splay

首先明确:每一个splay维护的是一个深度严格单调递增的链(简单说就是一个严格从祖先到后代的链)。用实链连接起来的一堆点在同一个splay里面。所以整个森林实际上也有很多个splay。

所以说实边虚边在LCT中的体现实际上就是在不在一个splay里面。

每个splay结点维护的值是其在splay中的整个子树的信息和。(比如取最大最小啦,求和或者异或啦)

因为维护的是信息和,所以在作修改的时候为了不影响到别的点,你要先splay上去。

这样就方便我们调用。至于为什么方便,我也说不清楚(手动滑稽)。

基本函数学习

1.access

access函数是非常基本的函数。非常重要。它的意义是:将该点到根节点的这个路径上的所有链变成实链。

这句话的潜含义有二。

1.因为每个点只有一条向下的实链,所以实际上它是将一些实边转换成了轻边,然后硬生生的把一些虚边变成了实边。

2.因为splay维护实边连接起来的点,所以现在,根节点到该节点路径上的所有点包括它们二位都在一个splay里面了。

既然在一个splay里面了,那,就好办了噻嘿嘿。通过完美的pushup和pushdown,我们就能轻松调用里面的信息了。

相信我,代码比你们想的简单多了。

void access(int x){
	for(int y;x;y=x,x=t[x].fa){//y永远是x下面的那个点
		splay(x);//这个时候是没有右儿子的了。
        t[x].ch[1]=y;//所以现在有了ovo
        pushup(x);
	}
}

2.makeroot

makeroot的含义是,将这个点变成所在树的根,也就是换根。怎么办呢QAQ?

首先,为了让这位可怜的点点和十万八千里外的根节点联系上,我们进行的第一步是:access。

现在它们在一个splay里面啦!然后呢?然后我们会注意到这么一个性质。

由于splay按照深度严格递增维护,所以access完后的这个点是splay里面深度最深的点,换句话说

将这个点splay到根节点,它就不会有右儿子了ovo

于是我们splay了。emm然后?既然我们要换根,那如何在splay里面,把这个点变成深度最浅的那个根呢?

所以我们用上了大名鼎鼎的:翻转打标记。想想看,splay完后的这个splay实际上很左倾(滑稽),但翻转完后,就变成彻彻底底的右倾了(滑稽)。

是的,总的来说,access,splay,打标记。

void makeroot(int x){
	access(x);splay(x);push(x);//用来打标记的函数
}

3.findroot

findroot顾名思义,找这个点所在树的根节点。

同样,为了和根节点擦除爱的火花,我们要先access一下。然后splay。

还是一个左倾树(乱编的名字),那我们就一直找左儿子,一直找下去就可以啦!记得下传标记哦!

顺带一提,为了防止pushup对splay上维护的值产生影响,我们要把这个点splay后才能pushup。

int findroot(int x){
	access(x);splay(x);
	while(t[x].ch[0]){
		pushdown(x);
		x=t[x].ch[0];
	}splay(x);
    pushup(x);
	return x;
}

4.link

链接操作,非常优秀。

对于两个点要链接,首先我们还是要makeroot其中一个点。然后要判重的话,就要判findroot另一个点的返回值是不是上一个点,是,说明有边不需要了,不是,说明要连边。

void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x)t[x].fa=y;//还是认父不认子
}

5.cut

删边操作,也很优秀。

对于两个点要删边,首先我们还是要makeroot其中一个点。然后access,splay都是常规操作。然后如果要判是否有边的话,就要判断三个东西。

第一是findroot另一个点是不是上一个点,注意findroot完后,上一个点又又被splay成了根,覆盖了splay(y)

第二个自然是这个点的父亲是不是上一个点。

第三个就是这个点不能有左儿子。

为什么呢?

因为findroot第二个点的时候把第一个点旋转成了splay 的根,则第二个点现在是第一个点的右儿子。而如果两者有边,说明二者深度必然是+1关系的,那在splay上,根的右儿子不能有左儿子。

void cut(int x,int y){
	makeroot(x);access(y);splay(y);
	if(findroot(y)==x&&t[y].fa==x&&!t[y].ch[0]){
		t[y].fa=t[x].ch[1]=0;
		pushup(x);//记得更新
	}
}

6.split

提取操作,非常优秀。

提取指定两点间的链,方便查询,思想跟树剖有点相似。具体而言就是makeroot,access,splay,查询。

int split(int x,int y){
	makeroot(x);access(y);splay(y);
	return t[y].pos;//因题而异
}

7.splay变化了。

由于LCT的性质,导致翻转标记有时候可能在上面没有下传。所以splay之前,我们有一个下传标记的工作。

具体而言就是开一个栈,不断跳父亲把能装的都装进去,然后从上至下pushdown。

void splay(int x){
	cnt=0;int y,z;
	y=x;sta[++cnt]=y;
	while(noroot(y)){
		y=t[y].fa;sta[++cnt]=y;
	}
	while(cnt){
		pushdown(sta[cnt--]);
	}
	while(noroot(x)){
		if(noroot(y)){
			(t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
		}
		rotate(x);
	}
	pushup(x);
}

同样与splay本身不同的是,此处我们判定旋转到根的意义就是,根的父亲的儿子不是根。(实边定义)(认父不认子)

所以noroot操作就是这样的:

int noroot(int x){
	return t[t[x].fa].ch[0]==x||t[t[x].fa].ch[1]==x;
}

同时在rotate里面也要用noroot判断。

void rotate(int x){
	int y=t[x].fa,z=t[y].fa,k=t[y].ch[1]==x,w=t[x].ch[k^1];
	if(noroot(y)){
		t[z].ch[t[z].ch[1]==y]=x;
	}t[y].ch[k]=w;t[x].ch[k^1]=y;
	if(w)t[w].fa=y;t[y].fa=x;t[x].fa=z;
	pushup(y);pushup(x);
}

另外顺带一提,在LCT里面,我们建议pushdown的时候,每次都把自己孙子给push了。

也就是这样:

void push(int x){
	swap(t[x].ch[0],t[x].ch[1]);t[x].tag^=1;
}
void pushdown(int x){
	if(t[x].tag){
		t[x].tag=0;
		if(t[x].ch[0])push(t[x].ch[0]);
		if(t[x].ch[1])push(t[x].ch[1]);
	}
}

据说这样很严谨不会被卡?

pushup就是基本的操作就不写了。

例题可以参照洛谷省选斗兽场的动态树。

放一个LCT洛谷模板的代码。

#include<bits/stdc++.h>
#define in read()
#define il inline
#define lc t[x].ch[0]
#define rc t[x].ch[1]
using namespace std;
int in{
    int cnt=0,f=1;char ch=0;
    while(!isdigit(ch)){
        ch=getchar();
        if(ch=='-')f=-1;
    }
    while(isdigit(ch)){
        cnt=cnt*10+ch-48;
        ch=getchar();
    }
    return cnt*f;
}
int n,m;
int a[300003];
struct node{
    int fa,ch[2],tag,val;
}t[300003];
int noroot(int x){
    return t[t[x].fa].ch[0]==x||t[t[x].fa].ch[1]==x;
}
void pushup(int x){
    t[x].val=t[lc].val^t[rc].val^a[x];
}
void push(int x){
    swap(lc,rc);t[x].tag^=1;
}
void pushdown(int x){
    if(t[x].tag){
        if(lc)push(lc);
        if(rc)push(rc);
        t[x].tag=0;
    }
}
void rotate(int x){
    int y=t[x].fa,z=t[y].fa,k=t[y].ch[1]==x,w=t[x].ch[k^1];
    if(noroot(y)){
        t[z].ch[t[z].ch[1]==y]=x;
    } 
    t[x].ch[k^1]=y;t[y].ch[k]=w;
    if(w)t[w].fa=y;t[y].fa=x;t[x].fa=z;
    pushup(y);pushup(x);
}
int sta[300003],cnt;
void splay(int x){
    cnt=0;int y=x;sta[++cnt]=y;
    while(noroot(y))sta[++cnt]=y=t[y].fa;
    while(cnt){
        pushdown(sta[cnt--]);
    }
    while(noroot(x)){
        y=t[x].fa;int z=t[y].fa;
        if(noroot(y)){
            (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
        }rotate(x);
    }
    pushup(x);
}
int access(int x){
    for(int y=0;x;y=x,x=t[x].fa){
        splay(x);t[x].ch[1]=y;pushup(x);
    }
}
int makeroot(int x){
    access(x);splay(x);push(x);
}
int findroot(int x){
    access(x);splay(x);
    while(lc)pushdown(x),x=lc;
    splay(x);
    return x;
}
int link(int x,int y){
    makeroot(x);
    if(findroot(y)!=x)t[x].fa=y;
}
void split(int x,int y){
    makeroot(x);access(y);splay(y);
}
int cut(int x,int y){
    makeroot(x);
    if(findroot(y)==x&&t[y].fa==x&&!t[y].ch[0]){
        t[y].fa=t[x].ch[1]=0;
    }
}
int main(){
    n=in;m=in;
    for(int i=1;i<=n;i++){
        a[i]=in;
    }
    while(m--){
        int opt=in,x=in,y=in;
        switch(opt){
            case 0:split(x,y);printf("%d\n",t[y].val);break;
            case 1:link(x,y);break;
            case 2:cut(x,y);break;
            case 3:splay(x);a[x]=y;
        }
    }
    return 0;
}

解释一下,对于3号操作,修改操作,为了防止修改完后对平衡树造成不当影响,我们先把它转到根,再修改。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值