Link Cut Tree 学习笔记

发布时间 2023-07-08 07:42:51作者: 霜木_Atomic

Link Cut Tree

这里推荐另一位大佬的博客,这篇博客对 LCT 进行了详细的讲解。Link
本篇博客仅用于个人学习记录,可能有的地方写的不够细致准确,还请谅解 uwu;如有谬误,欢迎指出。

引入:

Link Cut Tree (以下简称 LCT)是一种用来维护动态树的数据结构,通俗地讲,LCT 可以做到把一棵树拆成数条链,并对这些链进行较为灵活的操作。

思想&实现

LCT 本质上是一片 Splay 森林,关于为什么用 Splay,因为 Splay 的特点就是非常灵活,不需要对根有特别要求。
对于这片 Splay 森林,其中的每一个节点都对应原树上一个真实节点。我们把原树的边划分为实边与虚边,那么每一个由实边构成的链就对应一颗 Splay,而 Splay 的中序遍历维护链上节点由浅至深的遍历顺序。那么虚边呢?对于原树上虚边相连的两个节点,在 Splay 森林中体现的是将子节点所在的 Splay 的根的父亲设为父节点在其 Splay 中对应的节点。注意,这里的父子节点都指的是原树上的节点。

isroot 函数

那么,我们如何来区分一个节点是否为根呢?因为根和其父节点不属于同一棵 Splay,所以我们只需要判断一个节点的父亲的左右儿子是否有该节点即可,这也是我们要用到的第一个函数:

struct node{
	int fa, ch[2];
	bool rev_tag;//翻转标记,下文会用到
	int lazy;//需要维护信息的懒标记
	int val, siz, var;//维护的信息,子树大小,该点的值
}tr[N];
#define ls(x) tr[x].ch[0]
#define rs(x) tr[x].ch[1]
#define fa(x) tr[x].fa
inline bool isroot(int x){
	return ls(fa(x))!=x && rs(fa(x))!=x;
}

Splay

然后就是正常的 Splay 部分。还是注意这里根的判别方式。

inline void reverse(int x){
	swap(ls(x), rs(x));
	tr[x].rev_tag ^=1;
}//下面会用到
void push_down(int x){
	if(tr[x].rev_tag){
		if(ls(x)) reverse(ls(x));
		if(rs(x)) reverse(rs(x));
		tr[x].rev_tag = 0;
	}
	//另一组需要维护的东东 
}
void push_up(int x){
	tr[x].siz = tr[tr[x].ch[0]].siz+tr[tr[x].ch[1]].siz+1;
}
void rotate(int x){
	int y = tr[x].fa, z = tr[y].fa;
	int k1 = (tr[z].ch[1] == y), k2 = (tr[y].ch[1] == x);
	if(!isroot(y)) tr[z].ch[k1] = x;
	tr[x].fa = z;
	tr[y].ch[k2] = tr[x].ch[k2^1];
	tr[tr[x].ch[k2^1]].fa = y;
	tr[x].ch[k2^1] = y;
	tr[y].fa = x;
	push_up(y), push_up(x);
}

void update(int x){
	if(!isroot(x)) update(fa(x));
	push_down(x);
}//对树进行操作之前,必须把懒标记从头至尾全部下放。
void splay(int x){
	update(x);
	while(!(isroot(x))){//不同点
		int y = tr[x].fa, z = tr[y].fa;
		if(!isroot(y)){
			((tr[z].ch[0]==y)!=(tr[y].ch[0]==x))?rotate(x):rotate(y);
		}
		rotate(x);
	}
}

access 函数

这个函数的含义是,把当前节点到原树上的根之间的路径变为实边,原来与这条路径上节点相连的非路径边都变成虚边。这样我们就能够抽出一条链来。

inline int access(int x){
	int t = 0;
	for(t = 0; x; t = x, x = fa(x)){
		splay(x), rs(x) = t, push_up(x);
	}
	return t;//返回最后的根。
}

make_root 函数

这个函数用来转换原树的根。我们来考虑转换根后会发生的变化,发现只会将当前节点到根路径上所有节点的父子关系翻转,也就是,把这条链的先序遍历翻转,也就是区间翻转。这时候就用上之前写的 reverse 函数了。

inline void make_root(int x){
	x = access(x);
	reverse(x);
}

find_root 函数

既然原树上的根是不确定的,那么我们如何定位一个节点所在原树的根呢?答案是先 access 一次,然后一直向左子树走,根据二叉搜索树的性质,最左侧的点一定是深度最浅的点,也就是根。

inline int find_root(int x){
	x = access(x), push_down(x);
	while(ls(x)) x = ls(x), push_down(x);
	splay(x); return x;
}

总算见到主角辣(
link 函数很好理解,就是把两棵原来不相连的树链接起来,注意要判断原来是否是同一棵树。

inline void link(int x, int y){
	make_root(x);
	if(find_root(y)!=x) splay(x), tr[x].fa = y; 
}

cut 就是 link 的反向操作:切断两个节点之间的边,让他们所在的树分裂为两棵树。注意这里不仅要判断原来是否在一棵树,还要判断在原来树上是否有边相连(不能把相隔很远的两个节点断开)。

inline void cut(int x, int y){
	make_root(x);
	if(find_root(y) == x && fa(y) == x && !ls(y)){
		tr[y].fa = tr[x].ch[1] = 0, push_up(x);
	}
}

split 就是在原树上分离出两个节点之间的路径。

inline void split(int x, int y){
	make_root(x);
	access(y);
	splay(y);
}