推荐博客
想要学LCT的还是看这几篇比较好。我这篇只是总结一些容易理解错的或者一不小心打错的地方。
概述
LCT(link cut tree),就是又可以link(动态加边)又可以cut(动态删边)的维护一片森林的数据结构。
LCT使用实链剖分,对每一条实链用Splay维护(一棵Splay叫一棵辅助树),实链之间用虚边相连。
LCT的基本操作是access,作用是把一个点和根之间用实链连接起来,其他操作都可以在access和splay的基础上完成。
实链剖分
了解LCT的结构。
先贺两张图,来自flashHu
前一张是原树,后面的将实链表示成了Splay的形式。
可以发现在LCT的结构中,每棵Splay按照深度排序,单棵Splay的root不一定是这条链上最浅的点 (显然),但是他的父亲指针一定指向原树中这条链链顶的父亲节点。
功能汇总
这些是一棵正常的 LCT 免不了要实现的功能。
辅助的 3 个
- son
- is_root
- reverse
更新数组 3 个
- push_up
- push_down
- pre_push_down
Splay 基本操作 2 个
- rotate
- splay
LCT 基本功能
- access
- make_root
其他可能要用到的
- find_root
- split
- link
- cut
再其余的功能一般都是基于上面的函数操作的。
access
众所周知,access(u)的目的是将u与根放在同一个Splay中,并使u是这条实链上最深的点,即将 root-u 的路径上的点全部变成实边,u和儿子的边全部变成虚边。
那这时候Splay 支持瞎jb乱旋 灵活地维护区间的优点就显现出来了。
看代码(尽量写成了平易近人的样子):
void access(int u){
int v = 0;
while (u){
splay(u);
ch[u][1] = v;
push_up(u);
v = u;
u = fa[u];
}
}
自然语言复述:
- 把u旋转到当前Splay的root
- 然后断掉他的右儿子,这样u就是这条实链中最深的节点了。
- 在u的右儿子位置接上上一个Splay,那么一条新的实链就产生了
- 把u变成u所在实链链顶在原树上的父亲,重复过程。
可以看出2和3步骤只用了一句ch[u][1] = v
就完成了,因为判断一个点u是Splay的根只需要fa[u]的两个儿子都不是u就行了,所以连上新实边的同时也就断掉了旧实边。
有了access之后其他操作就都不难实现了。
is_root()
用于判断一个点是不是他所在的 Splay 的根。
bool is_root(int x){return ch[fa[x]][0] != x && ch[fa[x]][1] != x;}
因为在 LCT 中,一棵 Splay 中非根节点的 fa 指向他 Splay 中的父亲,而一棵 Splay 的根 root 的 fa 指向的是 root 这棵 Splay 中最浅的点的父亲,不是这棵 Splay 中的点。
依据这个性质可以方便地判断一个点是不是当前 Splay 的根。
在 rotate 的时候要用到。
push_up() 和 push_down()
何时更新节点信息是非常关键的。
对于 push_up(),只要儿子变了就需要 push_up()。
然后对于 push_down(),由于一个懒标记能影响的只有打标记时他所在 Splay 的子树,后来连上的点是不能够取影响的,所以在 access 将要连上一条实边的时候必须保证深度浅的 Splay 根上没有标记。
与一般 Splay 不同的是,在一般平衡树上把一个点旋转到根之前必然是要利用二叉查找树的性质寻找这个节点,顺便就把懒标记推下去了,但是 LCT 可以随便找一个节点就让你 Splay 到根,那么懒标记怎么办呢?
所以我们不得不暴力一点了,在想要将一个点Splay到根之前先暴力将根到他的路径懒标记全都推掉,虽然复杂度不会变(反正要一个一个 rotate 上去),但是自带大常数还是免不了了。
rotate的变化
这是LCT中Splay的rotate:
void rotate(int u){
int v = fa[u], w = fa[v];
int vs = son(u), ws = son(v);
if (!is_root(v)) ch[w][ws] = u;
fa[v] = u;
ch[v][vs] = ch[u][vs^1];
fa[ch[u][vs^1]] = v;
fa[u] = w;
ch[u][vs^1] = v;
push_up(v); push_up(u);
}
好像在单纯的 Splay 的 rotate 中并没有加判断呀,为什么要加这一句呢?
可以 YY 一下,假如 is_root(v)==true
,那么w就不是这棵 Splay 上的点,那么 ch[w][ws]=u
的意义是什么呢?不就是把 u 所在的 Splay 和 w 所在的 Splay 合并了吗。。。所以显而易见 FST 。
代码
luogu板子
#include<bits/stdc++.h>
using namespace std;
namespace LCT
{
const int N = 3e5+10;
int n, fa[N], ch[N][2], val[N], sum[N], flp[N];
inline bool is_root(int x){return ch[fa[x]][0] != x && ch[fa[x]][1] != x;}
inline int son(int x){return ch[fa[x]][1] == x;}
inline void reverse(int u){swap(ch[u][0], ch[u][1]); flp[u] ^= 1;}
inline void push_up(int u){sum[u] = sum[ch[u][0]] ^ sum[ch[u][1]] ^ val[u];}
inline void push_down(int u){
if (flp[u]){
if (ch[u][0]) reverse(ch[u][0]);
if (ch[u][1]) reverse(ch[u][1]);
flp[u] = 0;
}
}
void pre_push_down(int u){
if (!is_root(u)) pre_push_down(fa[u]);
push_down(u);
}
inline void rotate(int u){
int v = fa[u], w = fa[v];
int vs = son(u), ws = son(v);
if (!is_root(v)) ch[w][ws] = u;
fa[v] = u;
ch[v][vs] = ch[u][vs^1];
fa[ch[u][vs^1]] = v;
fa[u] = w;
ch[u][vs^1] = v;
push_up(v); push_up(u);
}
inline void splay(int u){
pre_push_down(u);
for (; !is_root(u); rotate(u))
if (!is_root(fa[u]))
rotate(son(u) == son(fa[u]) ? fa[u] : u);
}
inline void access(int u){
for (int v = 0; u; v = u, u = fa[u])
splay(u), ch[u][1] = v, push_up(u);
}
inline void make_root(int u){
access(u); splay(u); reverse(u);
}
}
using namespace LCT;
int m;
int find_root(int x){
access(x);
splay(x);
while (ch[x][0]) x = ch[x][0], push_down(x);
return x;
}
void split(int x, int y){
make_root(x);
access(y);
splay(y);
}
int get_sum(int x, int y){
split(x, y);
return sum[y];
}
void link(int x, int y){
if (find_root(x) == find_root(y)) return;
make_root(x);
fa[x] = y;
}
void cut(int x, int y){
if (find_root(x) ^ find_root(y)) return;
split(x, y);
if (ch[x][1]) return;
ch[y][0] = fa[x] = 0;
}
void change(int x, int y){
splay(x);
val[x] = y;
push_up(x);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++ i)
scanf("%d", &val[i]), sum[i] = val[i];
for (; m--; ){
int opt, x, y;
scanf("%d%d%d", &opt, &x, &y);
if (opt == 0) printf("%d\n", get_sum(x, y));
else if (opt == 1) link(x, y);
else if (opt == 2) cut(x, y);
else change(x, y);
}
return 0;
}
一些注意
犯过的错误。
push_up 的时候注意不是区间合并,父亲节点也是有权值的,不能把父亲给漏了~~(废话)~~
pre_push_down 不要递归,手写栈避免 MLE (in 弹飞绵羊)。
闲着没事就想想哪里该 push_down 了。一般功能应该都记得哪里该 push_down ,但是额外写的要特别注意。
例题
这里总结了好多好多完全做不完:xzyxzy
大概先写掉Qtree和luogu的试炼场就可以入门了吧。。。