前置知识
attack 学长的博客园的好像挂掉了,在这再整理一下 = =
几乎都是抄的==
定义
LCT是一种解决动态树问题的数据结构,由 tarjan 提出。
解决问题
- 求 LCA
- 求最小生成树
- 维护链上信息(最大最小,链上求和等)
- 维护联通性
- 维护子树信息
优化单纯的算法
构造
树上的剖分有三种:
- 重链剖分,树剖,原理是按照子节点的大小进行剖分。
- 长链剖分,没学过,原理是最深的儿子当做重儿子
- 实链剖分,LCT 运用的方法。
实链剖分
原理:选择一个点当作实儿子,把连接两个节点的边作为实边,连接其他儿子的边叫做虚边。
与其他两种剖分方式不同的是实链剖分的实儿子是不断变化的,因此在整个树中 的实链和虚链也是不断变化的。
我们用 \(splay\) 维护每一条实路径(仅由实边组成的路径),因为每条实路径都对应一条从根节点出发的链,所以路径上的每个节点的深度都是不同的
因此在 \(splay\) 中,按照深度排个序,对于每一个节点,左孩子所对应的原节点深度都比它小,右儿子所对应的原节点深度都比它大,这样保证了对一棵 \(splay\) 树进行中序遍历时,点的顺序正好是原实链中点从上到下的顺序。
这样每个节点都包含在了 \(splay\) 中,考虑如何让每个 \(splay\) 之间建立联系。对于一个节点,假设它有三个儿子,最多有一个节点和它在同一个 \(splay\) 中,其他的两个儿子在他们所在的子树中一定是深度最小的,因此可以让每个 \(splay\) 的根节点指向 \(splay\) 中中序遍历最小(原树中深度最小)的节点的父亲。
对于一棵这样的树:
构建出来的 \(splay\) 树:
LCT基本操作
access(x)
将根节点到 \(x\) 点的路径变成实路径,且 \(x\) 与其儿子之间的边都变成虚边。
目的:可以将根节点到 \(x\) 的这条路径放在同一棵 \(splay\) 中,这样就可以很方便的通过在 \(splay\) 上打标机得到路径信息。
对于上面那张图,如果找执行 \(access(N)\)
可以从下向上处理,首先使得 \(N-O\) 这条边变为虚边,因为在 \(splay\) 中,节点的深度是唯一的,所以我们首先把 \(N\) ,转到根节点,这样它右边的节点一定是 \(O\) ,然后直接把 \(N\) 号点的右儿子置为 \(0\) ,就好了。(这条边就会成虚边)。
继续向上,我们需要使 \(I-K\) 这条边变为虚边,同时使 \(I-L\) 这条边变为实边。
如何使得 \(I-K\) 变为虚边:同上,首先使 \(I\) 转到所在 \(splay\) 的根节点,这样它的右儿子一定是 \(K\) ,然后只需要断开 \(I-K\) 这条边就好。
如何使得 \(I-K\) 变为实边:直接将 \(I\) 的右儿子置为 \(L\) 即可。
此时 \(I\) 所在的 \(splay\) 的父亲指向 \(H\)
同样操作,把 \(H-J\) 变为虚边,使 \(H-I\) 变为实边。
此时 \(H\) 指向 \(A\) ,然后 \(A\) 旋转到根节点,把右儿子置为 \(H\) 即可。
这样就实现了 \(access\) 操作,算法流程:
- 将要处理的节点转到根
- 更改右儿子
- 更新标记
code
void access(int x) {
for (int y = 0; x; x = fa(y = x))
splay(x), rs(x) = y, update(x);
}
扩展:
求 \(x,y\) 的 \(lca\) ?
两次 \(access\) (access(x), access(y)) 操作每次返回一个 \(y\) ,第二次的返回值就是就是 \(x\) 和 \(y\) 的 \(lca\) 。
原理:每次的返回值都是最后一次虚实变换时虚边父亲节点的编号。手摸一下代码就知道了。
makeroot(x)
将 \(x\) 成为原树的根。
操作意义: 对于多数树上询问,都是询问一条经过根节点的路径,由于 \(LCT\) 的构造缘故,这种询问处理很麻烦,但是询问的一段在根节点就很好处理。
实现思路:
首先需要 \(access(x)\) ,这样就把根节点和 \(x\) 放在了同一棵 \(splay\) 中。
此时 \(x\) 不含右儿子(没有深度比他大的点)
然后需要 \(splay(x)\) ,此时 \(x\) 就会成为 \(splay\) 的根节点,且 \(x\) 不含右儿子。
因为根节点所在的 \(splay\) 中,根节点没有左儿子(没有深度比他小的点)
直接将 x 的左右子树翻转
void makeroot(int x) {
access(x);
splay(x);
T[x].r ^= 1;
}
findroot(x)
找到 \(x\) 所在原树的根
因为 \(LCT\) 维护的是森林,所以说有很多树,因为每个节点所在原树的根节点往往是不同的。
操作意义: 可以判断两个点是否联通,判断两个点所在原树的根是否相同就好了。
先 \(access(x)\) ,然后再 \(spaly(x)\) ,再不断的往左走,这样就可以找到 \(x\) 所在树的根节点了。
往左走的时候要下传标记。
int find(int x) {
access(x); splay(x);
pushdown(x);
while(ls(x))pushdown(x = ls(x));
return x;
}
split(x, y)
访问原指定的一条在原树种的链。
\(split(x, y)\) 定义为拉出 \(x-y\) 的路径成为 splay
void split(int x, int y) {
makeroot(x);
access(y); splay(y);
}
\(x\) 成为根,那么 \(x\) 到 \(y\) 的路径可以用 \(access(y)\) 直接拉出来,将 \(y\) 转到 \(splay\) 根后,可以直接通过访问 \(y\) 来获取该路径上的信息。
link(x, y)
连接 \(x, y\)
首先 \(makeroot(x)\) ,然后\(access(y), splay(y)\) ,然后把 \(x\) 的父亲指向 \(y\) 就好了。
为了连边合法,要判一下 \(x, y\) 的联通性。
void link(int x, int y) {
makeroot(x);
if(findroot(y) != x) fa(x) = y;
}
Cut(x, y)
断开 \(x-y\) 这条边
需要 \(split(x, y)\), 此时 \(x\) 的父亲是 \(y\) ,因为 \(y\) 是深度最大的点,所以它的左儿子是 \(x\) 。
如果不保证 \(x-y\) 相连。
需要加一堆判断:
- \(x, y\) 一定要在一棵树内
- \(x\) 的父亲是 \(y\),\(y\) 的左儿子是 \(x\) 。
- \(x\) 没有右儿子。
最后一条是保证 \(x\) 和 \(y\) 直接相连。
复杂度
均摊复杂度为 \(O(log^2N)\)
代码
/*
work by:Ariel_
Sorce:P3690 【模板】动态树(Link Cut Tree
Knowledge:LCT
Time:O(nlogn)
*/
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#include <algorithm>
#define ll long long
#define rg register
using namespace std;
const int MAXN = 1e5 + 10;
int read(){
int x = 0,f = 1; char c = getchar();
while(c < '0'||c > '9') {if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9') {x = x*10 + c - '0'; c = getchar();}
return x*f;
}
int N, M, a[MAXN];
namespace Link_Cut_Tree{
#define ls(k) ch[k][0]
#define rs(k) ch[k][1]
int ch[MAXN][2], rec[MAXN], fa[MAXN], sum[MAXN], st[MAXN], rev[MAXN];
void update(int k) {
sum[k] = sum[ls(k)] ^ sum[rs(k)] ^ a[k];
}
void push_down(int k) {//下传标记
if(!rev[k]) return ;
swap(ls(k), rs(k));
rev[ls(k)] ^= 1;
rev[rs(k)] ^= 1;
rev[k] = 0;
}
bool isroot(int x) {//判断是否为辅助树的根
return ch[fa[x]][0] != x && ch[fa[x]][1] != x;
}
bool ident(int x) {//判断左右孩子
return ch[fa[x]][1] == x;
}
void connect(int x, int _fa, int tag) {
fa[x] = _fa; ch[_fa][tag] = x;
}
void rotate(int x) {//旋转操作,注意辅助树的根
int y = fa[x], r = fa[y], yson = ident(x), rson = ident(y);
int b = ch[x][yson ^ 1];
fa[x] = r;
if(!isroot(y)) connect(x, r, rson);
connect(b, y, yson);
connect(y, x, yson ^ 1);
update(y), update(x);
}
void splay(int x) {
int y = x, top = 0;
st[++top] = y;
while(!isroot(y)) st[++top] = (y = fa[y]);
while(top) push_down(st[top--]);
for (int y = fa[x]; !isroot(x); rotate(x), y = fa[x])
if(!isroot(y))
rotate(ident(x) == ident(y) ? y : x);
}
void access(int x) {// x->root 间的虚边全变为实边
for (int y = 0; x; x = fa[y = x])
splay(x), rs(x) = y, update(x);
}
void makeroot(int x) {//换根
access(x);
splay(x);
rev[x] ^= 1;
}
int findroot(int x) {
access(x), splay(x);
push_down(x);
while(ls(x)) push_down(ls(x)), x = ls(x);
splay(x);
return x;
}
void link(int x, int y) {//连接两条边
makeroot(x);
if(findroot(y) == x) return ;
fa[x] = y;
}
void cut(int x, int y) {
makeroot(x);
if(findroot(y) != x || ls(y) || fa[y] != x) return ;
fa[y] = 0, rs(x) = 0;
update(x);
}
void Modify(int x, int v) {
splay(x);
a[x] = v;
}
int Query(int x, int y) {
makeroot(x);
access(y), splay(y);
return sum[y];
}
}
using namespace Link_Cut_Tree;
int main(){
N = read(), M = read();
for (int i = 1; i <= N; i++) a[i] = read();
while(M--) {
int opt = read(), x = read(), y = read();
if(opt == 0) cout<<Query(x, y)<<'\n';
else if(opt == 1) link(x, y);
else if(opt == 2) cut(x, y);
else Modify(x, y);
}
puts("");
return 0;
}