【学习笔记】Link Cut Tree

Link Cut Tree(LCT) 是一种用来解决动态树问题的数据结构。

前置芝士:Splay

部分参考:FlashHu 的博客OI Wiki - Link Cut Tree

一、实链剖分和 LCT

实链剖分:将原树的每条边分成实边虚边。实边中,儿子认父亲,父亲也认儿子;虚边中,儿子认父亲,但父亲不认儿子。实边相连得到实链。每条实链可用数据结构维护。每条边的实虚是可变的,因此我们要采用更为高级的数据结构——Splay

LCT:用 Splay 来维护动态的树链剖分,每条实链都用一个 Splay 维护。

LCT 可以干这些事:

  1. 在两点之间连一条边
  2. 删去两点之间的边
  3. 修改某个点/两点间路径的权值
  4. 查询某个点的权值/两点间路径上的权值和/异或和/……
  5. 指定某点为原树的根
  6. 判断两点之间是否连通
  7. ……

单次操作时间复杂度为 (operatorname{O}(log n))

二、性质

主要有三点,后面许多操作都基于这些性质(下文出现的“深度”均指节点在原树中的深度)。

  1. 每一个 Splay 维护的是一条从上往下、深度严格递增的路径;
  2. 每个节点包含且仅包含于一个 Splay中;
  3. 实边包含在 Splay 中,而虚边是由一棵 Splay 的指向该 Splay 中序遍历最靠前的节点(最左边的节点)在原树中的父亲(在原树中这条链的顶端的父亲)。

三、实现

1. 数组含义(以 P3690 为例)

int ch[N][2];//两个儿子
int val[N];//该节点的权值
int xs[N];//子树权值异或和
int fa[N];//父亲
int tag[N];//翻转标记
int stk[N];//数组模拟栈

2. Splay 基本操作

这些都是 Splay 基本操作。其中 splay 与平常我们写的有些不同,需要注意。

还有一个 notrt 函数,作用是:判断一个点 p 是否不是 p 所在 splay 的根,只需要判断是否和父亲实边相连(p 的父亲是否认 p)

inline int ident(int p) {
	return ch[fa[p]][1] == p;
}

inline void update(int p) {
	xs[p] = xs[ch[p][0]] ^ xs[ch[p][1]] ^ val[p];
}

inline bool notrt(int p) {
	return ch[fa[p]][0] == p || ch[fa[p]][1] == p;
}

inline void connect(int p, int f, int cc) {//父子相认,是实边
	fa[p] = f;
	ch[f][cc] = p;
}

inline void flip(int p) {//翻转函数,打上标记并交换这个节点的左右儿子
	if(!p) return;
	tag[p] ^= 1;//0^1=1,1^1=0 即若原来无则现在有,若原来有则现在无(区间翻转的性质)
	swap(ch[p][0], ch[p][1]);
}

inline void push(int p) {//下传标记,清空该节点的标记并传给两个儿子
	if(!tag[p]) return;
	tag[p] = 0;
	flip(ch[p][0]);
	flip(ch[p][1]);
}
inline void rotate(int p) {
	int q = fa[p], r = fa[q], cp = ident(p), cq = ident(q), w = ch[p][cp ^ 1];
	fa[p] = r;
	if(notrt(q)) ch[r][cq] = p;//特别要注意,若notrt不为真说明q-r是一条虚边,旋转之后应该还是虚边,不能认儿子
	connect(w, q, cp);
	connect(q, p, cp ^ 1);
	update(q);
}

inline void splay(int p) {
	int top, q;
	for(stk[top = 1] = q = p; notrt(q);) stk[++top] = q = fa[q];
	while(top) push(stk[top--]);//从上至下把根到这个点的路径上所有点的标记下传
	for(; notrt(p); rotate(p))//这里要判notrt,保证在当前Splay内
		if(notrt(q = fa[p])) rotate(ident(q) == ident(p) ? q : p);
	update(p);
}

3. LCT 基本操作

  • ( ext{access(p)})

作用:把 p 到根这条路径上的边全部变为实边,构成一棵Splay。

由于性质 1,该路径上其它链都要给这条链让路,也就是把路径上的每个点到该路径以外的点之间的实边变成虚边。

由于性质 3,虚边一定连结着一棵 Splay 的根。所以要先把 p 转到该 Splay 的根。

假设在转之前,p 和 q 之间有虚边相连(q 是一棵深度比 p 大的Splay 的根),说明 q 的父亲是 p,那么直接 ch[p][1]=q 即可。儿子变了,要更新节点信息。

然后 q=p,p 则向上爬,变为它的父亲,进入下一个 Splay。然后重复上述操作。

简而言之:

  1. 旋转到根
  2. 换右儿子
  3. 更新信息
  4. 向上爪巴

下面来模拟一下这个过程(由于笔者又菜又懒,直接用了 FlashHu 的文章中的图片):

( exttt{access(N)})

inline void access(int p) {
	for(int q = 0; p; p = fa[p]) {
		splay(p);
		ch[p][1] = q;
		update(q = p);//右儿子改变,需要更新
	}
}
  • ( ext{make_root(p)})

作用:把 p 变成原树中的根。

想让p成为原树中的根,就要让它与现在的根之间的路径全为实边。

access(p) 后,p 一定是 p 所在 Splay 中深度最大的点。因为 access 中第一遍循环 q=0ch[p][1]=qch[p][1]=0。也就是说右子树为空,根据性质 1 可知该 Splay 中无比 p 深度更大的节点。

根是树中深度最小的节点,但它是深度最大的,所以要将 p 所在的 Splay 整个翻转(使用类似线段树的懒标记)。

inline void make_root(int p) {
	access(p);
	splay(p);
	flip(p);
}
  • ( ext{find_root(p)})

作用:寻找x所在原树的树根,常用于判断连通性。

首先 access(p),让 p 和 root 处于同一 Splay。

由于根节点深度最小,它一定在 Splay 的最左边(中序遍历中最前面的节点)。把 p 旋到根,一直向左走直到没有左儿子。这个节点就是根节点。最后 splay(p) 保证复杂度正确。

inline int find_root(int p) {
	access(p);
	splay(p);
	for(; ch[p][0]; p = ch[p][0]) push(p);
	splay(p);
	return p;
}
  • ( ext{split(p,q)})

作用:将路径 p-q 上的所有边变为实边,成为一个Splay。

之前已经实现过 access,可以将原树的根到某节点的路径上的所有边变为实边,成为一个Splay。make_root(p),p就变成了原树的根节点。将路径 p-q 上的所有边变为实边就转化为将原树的根到q的路径上的所有边变为实边。

inline void split(int p, int q) {
	make_root(p);
	access(q);
	splay(q);//此时splay的根为q,整个链的信息可以直接从q获取
}
  • ( ext{link(p,q)})

作用:在 pq 之间连一条边。

原树的根节点是没有(虚边相连的)父亲的,那么让 p 成为原树根,父亲指向 q 就连接了 p 和 q。

inline void link(int p, int q) {
	make_root(p);
	if(find_root(p) ^ find_root(q)) fa[p] = q;//fdrt(p)=fdrt(q)说明两点联通,再连边不合法
	//此时这条边是虚边,不在同一splay,不能update
}
  • ( ext{cut(p,q)})

作用:断开连接 pq 的边。

首先判断 p,q 是否有边。 make_root(p),p 成为原树的根,深度最小。所以 q 若与 p 相连,q 一定在 p 的右子树内。由于性质 1,这棵 Splay 中序遍历中 p 和 q 一定要相邻才行。那就有三种情况:

  1. p,q不连通
  2. p,q连通,但q的父亲不是p
  3. q的左子树不为空
inline void cut(int p, int q) {
	make_root(p);
	if(find_root(q) ^ p || fa[q] ^ p || ch[q][0]) return;
	fa[q] = ch[p][1] = 0;
	update(p);
}

四、其他神奇的操作

咕咕咕

五、例题

模板题,所有操作前文已讲,直接上代码。

#include <cstdio>
#include <cstring>

using namespace std;

#define in inline
typedef long long ll;
in int max(int x, int y) {return x > y ? x : y;}
in int min(int x, int y) {return x < y ? x : y;}
in void swap(int &x, int &y) {x ^= y ^= x ^= y;}
#define rei register int
#define rep(i, l, r) for(rei i = l, i##end = r; i <= i##end; ++i)
#define repd(i, r, l) for(rei i = r, i##end = l; i >= i##end; --i)
char inputbuf[1 << 23], *p1 = inputbuf, *p2 = inputbuf;
#define getchar() (p1 == p2 && (p2 = (p1 = inputbuf) + fread(inputbuf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
in int read() {
	int res = 0; char ch = getchar(); bool f = true;
	for(; ch < '0' || ch > '9'; ch = getchar())
		if(ch == '-') f = false;
	for(; ch >= '0' && ch <= '9'; ch = getchar())
		res = res * 10 + (ch ^ 48);
	return f ? res : -res;
}
const int N = 1e5 + 15;

int ch[N][2], val[N], xs[N], fa[N], tag[N], stk[N], tot;

in int ident(int p) {
	return ch[fa[p]][1] == p;
}

in void update(int p) {
	xs[p] = xs[ch[p][0]] ^ xs[ch[p][1]] ^ val[p];
}

in void connect(int p, int f, int cc) {
	fa[p] = f;
	ch[f][cc] = p;
}

in void flip(int p) {
	if(!p) return;
	tag[p] ^= 1;
	swap(ch[p][0], ch[p][1]);
}

in void push(int p) {
	if(!tag[p]) return;
	tag[p] = 0;
	flip(ch[p][0]);
	flip(ch[p][1]);
}

in bool notrt(int p) {
	return ch[fa[p]][0] == p || ch[fa[p]][1] == p;
}

in void rotate(int p) {
	int q = fa[p], r = fa[q], cp = ident(p), cq = ident(q), w = ch[p][cp ^ 1];
	fa[p] = r;
	if(notrt(q)) ch[r][cq] = p;
	connect(w, q, cp);
	connect(q, p, cp ^ 1);
	update(q);
}

in void splay(int p) {
	int top, q;
	for(stk[top = 1] = q = p; notrt(q);) stk[++top] = q = fa[q];
	while(top) push(stk[top--]);
	for(; notrt(p); rotate(p))
		if(notrt(q = fa[p])) rotate(ident(q) == ident(p) ? q : p);
	update(p);
}

in void access(int p) {
	for(int q = 0; p; p = fa[p]) {
		splay(p);
		ch[p][1] = q;
		update(q = p);
	}
}

in void mkrt(int p) {
	access(p);
	splay(p);
	flip(p);
}

in int fdrt(int p) {
	access(p);
	splay(p);
	for(; ch[p][0]; p = ch[p][0]) push(p);
	splay(p);
	return p;
}

in void split(int p, int q) {
	mkrt(p);
	access(q);
	splay(q);
}

in void link(int p, int q) {
	mkrt(p);
	if(fdrt(p) ^ fdrt(q)) fa[p] = q;
}

in void cut(int p, int q) {
	mkrt(p);
	if(fdrt(q) ^ p || fa[q] ^ p || ch[q][0]) return;
	fa[q] = ch[p][1] = 0;
	update(p);
}

signed main() {
	int n = read(), q = read(), opt, x, y;
	rep(i, 1, n) val[i] = read();
	for(; q; --q) {
		opt = read(); x = read(); y = read();
		switch(opt) {
			case 0 : split(x, y); printf("%d
", xs[y]); break;
			case 1 : link(x, y); break;
			case 2 : cut(x, y); break;
			case 3 : splay(x); val[x] = y; break;
		}
	}
	return 0;
}

咕咕咕

原文地址:https://www.cnblogs.com/creating-2007/p/13330087.html