【学习笔记】Link Cut Tree

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

前置芝士:Splay

一、实链剖分和 LCT

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

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

LCT 可以干这些事:

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

二、性质

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

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

(一个蓝圈中的节点代表一棵Splay,右图中实线代表实边,虚线代表虚边)

根据这些性质可以推导:对于节点 \(u\),它最多只能有一个儿子和 \(u\) 在同一 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 基本操作

  • \(\text{\large{access(x)}}\)

作用:把 x 到 树根 这条路径上的边全部变为实边,路径上的点构成一棵Splay(不在这条路径上的点不能在这棵 Splay 中)。

假设 p,q 之间存在一条虚边,q 的深度大于 p。现在要把它变成实边。

由于性质 3,虚边一定连结着一棵 Splay 的根。所以 q 是一棵 Splay 的根。

在虚边变实的过程中必然有一些边由实变虚。好像不太好搞。。。

下图是原树形态,1 是树根。

下图是操作前的 Splay 形态。

现在要 access(5)。2,5 边要变实,5,6 边和 2,3 边要变虚。

暂且不管 5,6 。现在 \(p=2,q=5\),在 Splay 中只知道 2,3 的中序遍历关系,具体位置关系不知道。

把 2 旋转到(2 所在 Splay 的)根试试!

(这个旋转可能不符合 Splay 的旋转方式,但这样保证正确性同时消除歧义)

根据性质 1,3 号节点出现在 2 的右子树中最左边的节点。根据性质 3,操作后节点 2,3 之间的虚边是在 2,7 之间。那么直接把 2 的右儿子改成 5 就行了!

下图是整个 \(\text{access}\) 操作后的 Splay 形态。

也就是说,splay(p) 后直接 ch[p][1]=q 即可。儿子变了,要更新节点信息。

然后 q=p,q 成为了这棵 Splay 的根。p 则向上爬,变为它的父亲,进入下一个 Splay。这样 p,q之间又是一条虚边,然后重复上述操作。

简而言之:

  1. 旋转到根
  2. 换右儿子
  3. 更新信息
  4. 向上爪巴
inline void access(int p) {
	for(int q = 0; p; p = fa[p]) {
		splay(p);
		ch[p][1] = q;
		update(q = p);//右儿子改变,需要更新
	}
}

模拟一下这段代码,体会一下实边 5,6 是如何变成虚边的。

  • \(\text{\large{make\_root(p)}}\)

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

想让 p 成为原树中的根,就要让它与现在的根之 在同一个 Splay 中。否则如果到根的路径上存在虚边,p 与根之间一定存在严格的大小关系。只有消除这些虚边才有可能让 p 成为根。

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

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

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

make_root 后,p 不仅是树根,还是 p 所在 Splay 的根。

  • \(\text{\large{find\_root(p)}}\)

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

首先 access(p),让 p 和树根处于同一 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;
}
  • \(\text{\large{split(p,q)}}\)

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

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

注意 access(p),access(q) 是错误的,根据 access 的作用理解一下。

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

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

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

inline void link(int p, int q) {
	make_root(p);
   //make_root 中已经让 p 同时成为树根和该 Splay 的根
	if(find_root(p) != find_root(q)) fa[p] = q;//相等说明两点联通,再连边不合法
	//此时这条边是虚边,不在同一splay,不能update
}
  • \(\text{\large{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);
}

上面是 p,q 间是实边的情况。如果是虚边,q 一定是 q 所在 Splay 的根。根据性质 3,如果 q 的左子树不为空,q 指向的 p 并不是 q(而是 q 所在 Splay 中最左边的点)在原树中的父亲,pq 间也就没有边直接相连。

四、其他神奇的操作

咕咕咕

五、例题

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

#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\n", 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;
}

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

posted @ 2020-07-17 15:08  EverlastingEternity  阅读(260)  评论(0编辑  收藏  举报