LCT 学习笔记

一看见平衡树就头重脚轻想睡觉的病怎么治/fn

原理及基本操作

预备芝士:实链剖分,即将一棵树中的边分为实边和虚边,使得每个结点连向儿子的实边只有 0 0 0 1 1 1 条(若有,可称其连接的儿子为首选儿子)。实边连起来形成的链就是实链。

LCT 就是把树进行实链剖分后,把每一条实链用一个 Splay 维护,Splay 的中序遍历对应树上点深度由浅到深的路径;而对于每一条虚边 f a → x fa\to x fax,将 x x x 所在 Splay 的根的父亲设为 f a fa fa,但 x x x 既不是 f a fa fa 的左子节点也不是 f a fa fa 的右子节点,即“认父不认子”。

我们把每一个连通块内若干 Splay 组成的结构叫辅助树,由辅助树的构造可知,一个连通块只有一个点没有父节点,就是原树的根。

利用 Splay 的灵活性质,可以在 LCT 上动态维护一些树上的加边删边问题。

下面是一堆由简单到复杂的 LCT 基本操作函数:

i s r o o t ( x ) isroot(x) isroot(x)

判断x是否为它所在Splay的根,根据认父不认子的性质就可以判断。

#define isroot(x) (x!=ch[f[x]][0]&&x!=ch[f[x]][1])

r o t a t e ( x ) rotate(x) rotate(x)

跟 Splay 差不多,只是要注意第三行必须放在最前面判,因为后续会更新 f [ y ] f[y] f[y] 导致死循环。

inline void rotate(int x){
	int y=f[x],z=f[y],qwq=get(x);
	if(!isroot(y)) ch[z][y==ch[z][1]]=x;
	ch[y][qwq]=ch[x][qwq^1];
	if(ch[x][qwq^1]) f[ch[x][qwq^1]]=y;
	ch[x][qwq^1]=y,f[x]=z,f[y]=x;
	pushup(y),pushup(x);
}

s p l a y ( x ) splay(x) splay(x)

必须先从根开始下放所有标记再旋转,据说递归容易忘了写,所以这里使用栈:

inline void splay(int x){
	int tmp=x,top=0;
	while(!isroot(tmp)) st[++top]=tmp,tmp=f[tmp];
	st[++top]=tmp;
	while(top) pushdown(st[top--]);
	for(int fa;fa=f[x],!isroot(x);rotate(x))
		if(!isroot(fa)) rotate(get(fa)==get(x)?fa:x);
}

a s s e s s ( x ) assess(x) assess(x)

LCT 的核心操作,取出 x x x 到原树根的一条路径。实现上,把 x x x 转到当前 Splay 的根,将其首选孩子(即右子树)断开并接上已经连好的链,不要忘了时时 pushup 更新。

inline void assess(int x){
	for(int y=0;x;y=x,x=f[x]) 
		splay(x),ch[x][1]=y,pushup(x);
}

m a k e r o o t ( x ) makeroot(x) makeroot(x)

x x x 为它所在树的树根。先连一条 x x x 到当前根的路径,使得 x x x 和根在一个 Splay 中,然后把 x x x 转到根。结合图理解一下发现 x x x 到原根的路径上点的相对深度关系会改变,故 Splay 的中序遍历应该随之改变,所以在这里要进行区间翻转。

inline void makeroot(int x){
	assess(x),splay(x),rev(x);
}

f i n d r o o t ( x ) findroot(x) findroot(x)

找到 x x x 所在树的根。将 x x x 换到原树根所在 Splay 的根后,根据中序遍历的性质一直往左子树走,中间不要忘了时时 pushdown。找到跟之后要 s p l a y ( ) splay() splay() 一下来保证复杂度。

inline int findroot(int x){
	assess(x),splay(x),pushdown(x);
	while(ls) pushdown(ls),x=ls;
	return splay(x),x;
}

s p l i t ( x , y ) split(x,y) split(x,y)

x x x y y y 联通时,取出一条以 x x x y y y 为两端点的实链。将 x x x 换到根之后连一条 y y y 到根的路径即可,一般为了方便后续操作,会再多一步将 y y y 转到当前 Splay 的根。

inline void split(int x,int y){
	makeroot(x),assess(y),splay(y);
}

l i n k ( x , y ) link(x,y) link(x,y)

连一条 ( x , y ) (x,y) (x,y) 的边。连边之前要先判断 x x x y y y 是否已经联通,可以把 x x x 转到根后判断 y y y 所在树的根是不是 x x x。若不联通,直接连一条 y → x y\to x yx 的虚边即可。

inline void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x) f[x]=y;
}

c u t ( x , y ) cut(x,y) cut(x,y)

断开边 ( x , y ) (x,y) (x,y)。同样的,断开之前要先判断 x x x y y y 之间是否有边 (假设不能用 map),先 s p l i t ( ) split() split() 一下,此时 x x x 为原树根, y y y 为它们所在 Splay 的根,若 x x x y y y 之间有边,则 x x x 必定是 y y y 的左儿子且 x x x 的右儿子为空,很显然这个能判掉 x x x y y y 不在一个连通块的情况。若存在边,更新 y y y 的左儿子和 x x x 的父亲为空。

inline void cut(int x,int y){
	split(x,y);
	if(ch[y][0]==x&&!ch[x][1]) f[x]=ch[y][0]=0;
}

掌握了这一串函数就可以出门左转模板了qwq

一些例题

[AHOI2005] 航线规划

传送门

神奇的用 LCT 维护边双。

考虑离线,变删边为加边,若两点不连通则直接连接,否则就是在树上缩点。这个过程可以用并查集维护。进行了 l i n k ( ) link() link() 的基本操作之后,原树根和当前 Splay 的根均为 x x x,且 x x x y y y 之间已经连了实链。只要暴力递归以 x x x 的右儿子为根的子树,修改并查集,再断开 x x x 和右儿子即可。这样每个点至多被进行一次这样的修改,可以保证复杂度。

唯一要注意的是这里 a s s e s s ( ) assess() assess() 的写法要变一下, x x x 是跳到其父亲的并查集的代表元素而不是直接跳到父亲。当然其它操作也都是对并查集连通块的操作qwq

最小差值生成树

传送门

LCT 维护边的思路:把边变成点,连 u u u v v v 之间编号为 i i i 的点即为连接 ( u , i ) (u,i) (u,i) ( i , v ) (i,v) (i,v),删边同理。

这道题将边权从小到大排序,建出最小生成树之后再加边 ( u , v ) (u,v) (u,v) 时,每次删去连接 u u u v v v 的树边中边权最小的边,再加入 ( u , v ) (u,v) (u,v),易证这样总是最优的。具体地,在 LCT 上维护最小边权和最小边的编号,查找最小值可以先记录一下每条边是否被选,找被选的编号最小的边,用线段树队列即可维护。

边按权值大小赋上编号,而点实际上没有编号,但我们可以强制给它一个编号。在 pushup 的时候如果最小值是自己,要把编号复原,否则会出现一些神秘 RE。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值