Link Cut Tree(LCT) 是一种用来解决动态树问题的数据结构。
前置芝士:Splay
部分参考:FlashHu 的博客;OI Wiki - Link Cut Tree
一、实链剖分和 LCT
实链剖分:将原树的每条边分成实边和虚边。实边中,儿子认父亲,父亲也认儿子;虚边中,儿子认父亲,但父亲不认儿子。实边相连得到实链。每条实链可用数据结构维护。每条边的实虚是可变的,因此我们要采用更为高级的数据结构——Splay。
LCT:用 Splay 来维护动态的树链剖分,每条实链都用一个 Splay 维护。
LCT 可以干这些事:
- 在两点之间连一条边
- 删去两点之间的边
- 修改某个点/两点间路径的权值
- 查询某个点的权值/两点间路径上的权值和/异或和/……
- 指定某点为原树的根
- 判断两点之间是否连通
- ……
单次操作时间复杂度为 (operatorname{O}(log n))。
二、性质
主要有三点,后面许多操作都基于这些性质(下文出现的“深度”均指节点在原树中的深度)。
- 每一个 Splay 维护的是一条从上往下、深度严格递增的路径;
- 每个节点包含且仅包含于一个 Splay中;
- 实边包含在 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。然后重复上述操作。
简而言之:
- 旋转到根
- 换右儿子
- 更新信息
- 向上爪巴
下面来模拟一下这个过程(由于笔者又菜又懒,直接用了 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=0
,ch[p][1]=q
即 ch[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 一定要相邻才行。那就有三种情况:
- p,q不连通
- p,q连通,但q的父亲不是p
- 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;
}
咕咕咕