LCT 学习笔记

推荐博文

文艺平衡树

LCT 的前置知识当然时文艺平衡树啦。先讲讲文艺平衡树。

题意

链接

给你一个长度为 \(n\) 的序列 \(a_i\),\(T\) 次操作,每次翻转一个区间,输出最后的序列。

题解

维护一个 Splay,一开始按照 \(a_i\) 为下标,\(i\) 为键值 进行建树,可以理解成键值就是这个点的深度。

对于区间翻转 \([l,r]\),我们先将键值为 \(l-1\) 的点旋至根,再将键值为 \(r+1\) 的点旋至 \(l-1\) 的右儿子,这样由于平衡树左大右小的性质, \(r+1\) 点的子树就都是原序列 \([l,r]\) 内的节点了。

因为翻转后,最左的点与最右的点交换,第二左的点与第二右的点交换……也就是第 \(i\) 左的点与第 \(i\) 右的点交换,也就是第 \(i\) 浅的点与第 \(i\) 深的点交换。

所以,我们对这个子树每一个左右节点进行反转。这就相当于第 \(i\) 浅的点与第 \(i\) 深的点交换了。

注意:我们不会真的去更新键值,而是依靠一个点在平衡树中的位置去判断它在原序列哪一个位置。讨论

进行反转时间复杂度太高,所以打 LazyTag,像线段树一样,当访问到此节点时再执行反转操作并下推标记即可。

LCT 简介

略。

注意虚边连接的平衡树,代表着 这棵平衡树深度最小的点连接着上面的点

下文所说一个点的根指的是它所在连通块中,深度最小的点。

LCT 基操

Splay

Splay(o) 支持将点 \(o\) 旋转至它的平衡树的根。注意虚边是认父不认子的,所以根可能也有父亲。但是这父亲可不能连边,所以要特判。

void updat(int o){tre[o].tot=tre[lsn(o)].tot^tre[rsn(o)].tot^tre[o].val;}

bool Dwhi(int o){return rsn(tre[o].fa)==o;}

bool isrot(int o){return lsn(tre[o].fa)^o&&rsn(tre[o].fa)^o;}

void rota(int o){
	int y=tre[o].fa,z=tre[y].fa,whi=Dwhi(o);
	int fawhi=( isrot(y)?-1:Dwhi(y) ),v=tre[o].son[whi^1];
	tre[v].fa=y,tre[y].son[whi]=v;
	tre[y].fa=o,tre[o].son[whi^1]=y;
	tre[o].fa=z;if(~fawhi)tre[z].son[fawhi]=o;
	updat(y),updat(o);
}

void Splay(int o){
	puall(o);
	while( !isrot(o) ){
		int y=tre[o].fa;
		if( !isrot(y) ){
			Dwhi(o)==Dwhi(y)?rota(y):rota(o);
		}
		rota(o);
	}
}

access

access(o) 是 LCT 的核心操作:将点 \(o\) 到原树中 \(o\) 所在连通块的根的路径打包成一个实链。(当然,原来的某些实边可能会因此变成虚边)

首先将 \(o\) 旋转到所处平衡树的根,然后将它的右儿子设为 \(0\)(因为 \(o\) 到根的路径必定是越来越浅的,但是右儿子是深度比它大的,所以砍掉)。更新 \(o\) 的信息,然后设 \(las=o\),跳上 \(o\) 的父亲(即虚边上面的点)。

再次按照上面操作,但是将 \(o\) 的右儿子设为 \(las\)。直到 \(o\) 没有父亲为止。

void aces(int o){
	int las=0;
	while(o){
		Splay(o),rsn(o)=las,updat(o);
		las=o,o=tre[o].fa;
	}
}

有了 access ,我们就可以做很多很多操作了。

makeroot

makeroot(o) 将 \(o\) 设为原树中 \(o\) 所在的连通块的根。

这就相当于将先 access(o),将 \(o\) 和根的路径打通,然后反转路径上的所有父子关系。也就是第一深和第一浅交换,第二深与第二浅交换……

于是文艺平衡树的翻转标记来啦,Splay(o) 将 \(o\) 置为当前平衡树的根,然后打个翻转标记。

void Mroot(int o){aces(o),Splay(o),tre[o].laz^=1;}

findroot

findroot(o) 找到原树中 \(o\) 所在的连通块的根。

这就相当于找到连通块中深度最浅的点。

将 \(o\) 和根的路径打通,将 \(o\) 旋转至平衡树根,然后不断走向它的左儿子(左儿子深度浅)。

最后再 Splay(o),使平衡树保持平衡。

int Froot(int o){
	aces(o),Splay(o);
	while( lsn(o) )pudown(o),o=lsn(o);
	Splay(o);
	return o;
}

split

split(o1,o2) 将 \(o1\sim o2\) 的路径打包成一个实链,并将 \(o2\) 设为平衡树的根,前提是 \(o1,o2\) 在一个连通块中。

makeroot(o1),access(o2) 即可,最后 Splay(o2) 使平衡树保持平衡。

void split(int o1,int o2){Mroot(o1),aces(o2),Splay(o2);}

link(o1,o2) 在原树上连边 o1,o2(如果两点已经在一个连通块,则不操作)。

直接将 o1 设为当前连通块的根,然后他的父亲变成 o2 即可(其中还要判断下 o2 的根是不是 o1)。

o2 不需要认 o1 为儿子,因为这条新边默认为虚边,认父不认子。

void link(int o1,int o2){
	Mroot(o1);
	if(Froot(o2)==o1)return;
	tre[o1].fa=o2;
}

cut

cut(o1,o2) 在原树上删边 o1,o2(如果没有这条边,则不操作)。

这个操作判合法比较难。

首先如果 o1,o2 不在同一个连通块,那么显然不操作。

然后就把 o1,o2 放在一个 Splay 中(split)。

此时 o1 就是原树上连通块的根o2 就是平衡树的根了,如果 o1o2 之间连边,那么它们的深度必定相差 \(1\)。这意味着 o1o2左儿子(因为 o1 是根,最浅)。如果不是,那么不操作。

而且因为 o1 是一个实链的根,所以它在这个实链中只会与一个点连边(就是 o2),故 o1 理应没有儿子。所以再判断 o1 有没有任何儿子,如果有儿子就不操作。

最后经过重重判合法,我们直接让 o1,o2 断绝一切关系(o1 没有父亲、 o2 没有左儿子),并更新 o2 的信息。

void cut(int o1,int o2){
	if( Froot(o1)^Froot(o2) )return;
	split(o1,o2);
	if(tre[o1].fa^o2|| lsn(o1) || rsn(o1) )return;
	tre[o1].fa=lsn(o2)=0;
	updat(o2);
}

以上所有的操作时间复杂度均为 \(O(\log n)\),所以总共为 \(O(\ (n+T)\log n\ )\)。

例题

例题 及其代码:(都是上面的操作,就不讲了,记得一开始的建边也是 link

#include<bits/stdc++.h>
#define rep(i,x,y) for(int i=x;i<=y;++i)
#define lsn(o) tre[o].son[0]
#define rsn(o) tre[o].son[1]
using namespace std;
const int n7=101234;
struct mist{int val,tot,siz,fa,son[2];bool laz;}tre[n7];
int n,T,a[n7];

int rd(){
	int shu=0;bool fu=0;char ch=getchar();
	while( !isdigit(ch) ){if(ch=='-')fu=1;ch=getchar();}
	while( isdigit(ch) )shu=(shu<<1)+(shu<<3)+ch-'0',ch=getchar();
	return fu?-shu:shu;
}

void updat(int o){
	tre[o].tot=tre[lsn(o)].tot^tre[rsn(o)].tot^tre[o].val;
}

bool Dwhi(int o){return rsn(tre[o].fa)==o;}

bool isrot(int o){return lsn(tre[o].fa)^o&&rsn(tre[o].fa)^o;}

void pudown(int o){
	if(!tre[o].laz)return;
	tre[lsn(o)].laz^=1,tre[rsn(o)].laz^=1;
	swap( lsn(o),rsn(o) );
	tre[o].laz=0;
}

void puall(int o){
	if( !isrot(o) )puall(tre[o].fa);
	pudown(o);
}

void rota(int o){
	int y=tre[o].fa,z=tre[y].fa,whi=Dwhi(o);
	int fawhi=( isrot(y)?-1:Dwhi(y) ),v=tre[o].son[whi^1];
	tre[v].fa=y,tre[y].son[whi]=v;
	tre[y].fa=o,tre[o].son[whi^1]=y;
	tre[o].fa=z;if(~fawhi)tre[z].son[fawhi]=o;
	updat(y),updat(o);
}

void Splay(int o){
	puall(o);
	while( !isrot(o) ){
		int y=tre[o].fa;
		if( !isrot(y) ){
			Dwhi(o)==Dwhi(y)?rota(y):rota(o);
		}
		rota(o);
	}
}

void aces(int o){
	int las=0;
	while(o){
		Splay(o),rsn(o)=las,updat(o);
		las=o,o=tre[o].fa;
	}
}

void Mroot(int o){aces(o),Splay(o),tre[o].laz^=1;}

void split(int o1,int o2){Mroot(o1),aces(o2),Splay(o2);}

int Froot(int o){
	aces(o),Splay(o);
	while( lsn(o) )pudown(o),o=lsn(o);
	Splay(o);
	return o;
}

void link(int o1,int o2){
	Mroot(o1);
	if(Froot(o2)==o1)return;
	tre[o1].fa=o2;
}

void cut(int o1,int o2){
	if( Froot(o1)^Froot(o2) )return;
	split(o1,o2);
	if(tre[o1].fa^o2|| lsn(o1) || rsn(o1) )return;
	tre[o1].fa=lsn(o2)=0;
	updat(o2);
}

int main(){
	n=rd(),T=rd();
	rep(i,1,n)tre[i].val=rd(),updat(i);
	while(T--){
		int sys=rd(),p=rd(),q=rd();
		if(sys==0)split(p,q),printf("%d\n",tre[q].tot);
		if(sys==1)link(p,q);
		if(sys==2)cut(p,q);
		if(sys==3)Mroot(p),tre[p].val=q,updat(p);
	}
	return 0;
}
上一篇:LCT


下一篇:洛谷 P3721 - [AH2017/HNOI2017]单旋(LCT)