前言
很久没有写博客了,
Z
J
O
I
2019
ZJOI2019
ZJOI2019炸零快乐。
W
R
_
E
t
e
r
n
i
t
y
WR\_Eternity
WR_Eternity大佬已经早早地学会了
L
C
T
LCT
LCT这种强大的数据结构,这让只会树剖的本蒟蒻内心慌地一逼,于是我看着他的博客,勉强自学了
L
C
T
LCT
LCT。
至于时间复杂度的证明我这种蒟蒻当然只能选择略过了(逃。
蒟蒻自己的理解
树剖时我们选择重儿子是为了减少时间复杂度,但因为动态树中树的形态会变,那干脆就不要重儿子了,随便选一个儿子就好。
树剖时因为树剖序是固定的所以我们用线段树维护一般即可解决问题。但
L
C
T
LCT
LCT中会涉及到区间翻转,所以要用
S
p
l
a
y
Splay
Splay这种更为强大的数据结构做辅助树。
定义与约定
- P r e f e r r e d C h i l d Preferred\ Child Preferred Child:偏爱儿子,偏爱儿子与父亲节点在同一棵Splay中,每个节点最多只有一个偏爱儿子。
- P r e f e r r e d E d g e Preferred\ Edge Preferred Edge:偏爱边,连接父亲节点和偏爱儿子的边。
- P r e f e r r e d P a t h Preferred\ Path Preferred Path:偏爱链,由偏爱边及偏爱边连接的节点构成的链。
-
A
u
x
i
l
i
a
r
y
T
r
e
e
Auxiliary\ Tree
Auxiliary Tree:辅助树,由一条偏爱链上的所有节点所构成的
S
p
l
a
y
Splay
Splay称作这条链的辅助树。
每个点的键值为这个点的深度,即这棵 S p l a y Splay Splay 的中序遍历是这条链从链顶到链底的所有节点构成的序列
辅助树的根节点的父亲指向链顶的父亲节点,然而链顶的父亲节点的儿子并不指向辅助树的根节点(也就是说父亲只认偏爱儿子,儿子都认父亲) - 实树 题目中要维护的那堆树
- S p l a y Splay Splay中维护了子树的异或和、翻转标记。
一个大胆的假设
假设我们现在会两个操作。
- a c c e s s ( u ) access(u) access(u) 把 u u u 到实树根的路径变成偏爱路径,即把路径上的点都放到同一颗 S p l a y Splay Splay中。
- m a k e _ r t ( u ) make\_rt(u) make_rt(u) 把实树的根变成 u u u 。
解决各类操作
Q u e r y ( u , v ) Query(u, v) Query(u,v)
即查询
u
u
u ~
v
v
v 路径上点权的异或和。
我们直接把
u
u
u 当成实树的根,然后把
v
v
v 到根的路径塞到同一颗
S
p
l
a
y
Splay
Splay 中,然后把
u
u
u 节点
s
p
l
a
y
splay
splay 到辅助树的根,查询
u
u
u 的子树异或和即可。
int query(const int &u, const int &v) {
return make_rt(u), access(v), splay(v), sum[v];
}
F i n d _ r t ( u ) Find\_rt(u) Find_rt(u)
即查询
u
u
u 所在的实树的根节点。
我们把
u
u
u 到根节点上所有的点塞进一颗
S
p
l
a
y
Splay
Splay 中。
因为根的深度是最小的,所以把
u
u
u 节点
s
p
l
a
y
splay
splay 到辅助树的根,然后一直往左找即可,记得把标记下放即可。
int find_rt(int u) {
access(u), splay(u);
push_down(u);
while(son[u][0]) u = son[u][0], push_down(u);
return u;
}
L i n k ( u , v ) Link(u, v) Link(u,v)
即把
u
u
u 和
v
v
v 两个节点连在一起,若
u
u
u 和
v
v
v 已在同一棵树上则不做处理。
判断是否在同一棵实树中,只要判断实树的根是否相同即可。
若不在同一棵实树中,把
u
u
u 变成根,直接接成
v
v
v 的非偏爱儿子即可。
void link(const int &u, const int &v) {
if(find_rt(u) != find_rt(v)) make_rt(u), fa[u] = v;
}
C u t ( u , v ) Cut(u, v) Cut(u,v)
即把
u
u
u 节点和
v
v
v 节点之间的连边断开,若
u
u
u 和
v
v
v 不是直接相连则不做处理。
先判断是否有边相连。
把
u
u
u 变成根,把
v
v
v 到
u
u
u 之间的路径塞到一棵
S
p
l
a
y
Splay
Splay 中,再把
v
v
v 节点
s
p
l
a
y
splay
splay 到辅助树的根。
如果
u
u
u 与
v
v
v 之间有直接连边,因为
v
v
v 是 深度仅次于
u
u
u 的,那么此时
v
v
v 一定是
u
u
u 在辅助树上的父亲,而且
u
u
u 右子树为空,即没有深度介于
u
,
v
u,\ v
u, v 之间的点。
如果
u
,
v
u,\ v
u, v 之间有连边的话,将
v
v
v 的左儿子,
u
u
u 的父亲赋值为空并更新
v
v
v 节点的
s
u
m
sum
sum 即可。
void cut(const int &u, const int &v) {
make_rt(u), access(v), splay(v);
if(son[v][0] != u || son[u][1]) return;
fa[u] = son[v][0] = 0;
update(v);
}
实现假设
假设终究是假设,我们还是要靠自己的双手把 a c c e s s access access 和 m a k e _ r t make\_rt make_rt 实现的。
A c c e s s ( u ) Access(u) Access(u)
即把路径上的点都放到同一颗
S
p
l
a
y
Splay
Splay中。
要做的无非就两件事:
- 把该节点与原来的偏爱儿子断开
- 把该节点与父亲在辅助树上连起来
不断重复即可。
void access(int u) {
for(int v = 0; u; v = u, u = fa[u]) splay(u), son[u][1] = v, update(u);
}
M a k e _ r t ( u ) Make\_rt(u) Make_rt(u)
即把实树的根变成
u
u
u 。
很简单,把
u
u
u 到根的路径上的点塞到
S
p
l
a
y
Splay
Splay 中,把
u
u
u 节点
s
p
l
a
y
splay
splay 到辅助树的根,把整条链翻过来即可。
void make_rt(const int &u) {
access(u), splay(u), reverse(u);
}
代码
#include <cstdio>
#include <algorithm>
#define enter putchar('\n')
using namespace std;
const int maxn = 3e5 + 5;
int fa[maxn], son[maxn][2], sum[maxn], w[maxn], rev[maxn];
template <typename T>
void read(T &x) {
char ch = getchar(); bool f = 1; x = 0;
while(ch < '0' || ch > '9') f &= ch != '-', ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
x = f ? x : -x;
}
template <typename T>
void write(T x) {
if(x < 0) x = -x, putchar('-');
if(x > 9) write(x / 10);
putchar(x % 10 + 48);
}
inline bool get(int u) { return son[fa[u]][1] == u; }
inline bool is_rt(int u) { return son[fa[u]][0] != u && son[fa[u]][1] != u; }
inline void reverse(int u) { rev[u] ^= 1, swap(son[u][0], son[u][1]); }
inline void update(int u) { sum[u] = sum[son[u][0]] ^ w[u] ^ sum[son[u][1]]; }
void push_down(int u) {
if(rev[u]) {
if(son[u][0]) reverse(son[u][0]);
if(son[u][1]) reverse(son[u][1]);
rev[u] = 0;
}
}
void push_down_all(int u) {
if(!is_rt(u)) push_down_all(fa[u]);
push_down(u);
}
void rotate(int u) {
int f = fa[u], gra = fa[f], d = get(u), k = son[u][d ^ 1];
if(!is_rt(f)) son[gra][get(f)] = u;
fa[u] = gra, son[u][d ^ 1] = f;
fa[f] = u, son[f][d] = k;
fa[k] = f;
update(f), update(u);
}
void splay(int u) {
push_down_all(u);
for(; !is_rt(u); rotate(u))
if(!is_rt(fa[u])) rotate(get(u) == get(fa[u]) ? fa[u] : u);
}
void access(int u) {
for(int v = 0; u; v = u, u = fa[u]) splay(u), son[u][1] = v, update(u);
}
void make_rt(const int &u) { access(u), splay(u), reverse(u); }
int query(const int &u, const int &v) {
return make_rt(u), access(v), splay(v), sum[v];
}
int find_rt(int u) {
access(u), splay(u);
push_down(u);
while(son[u][0]) u = son[u][0], push_down(u);
return u;
}
void link(const int &u, const int &v) {
if(find_rt(u) != find_rt(v)) make_rt(u), fa[u] = v;
}
void cut(const int &u, const int &v) {
make_rt(u), access(v), splay(v);
if(son[v][0] != u || son[u][1]) return;
fa[u] = son[v][0] = 0;
update(v);
}
int main() {
int n, q;
read(n), read(q);
for(int u = 1; u <= n; u++) read(w[u]);
while(q--) {
int opt;
read(opt);
if(opt == 0) {
int u, v;
read(u), read(v);
write(query(u, v)), enter;
} else if(opt == 1) {
int u, v;
read(u), read(v);
link(u, v);
} else if(opt == 2) {
int u, v;
read(u), read(v);
cut(u, v);
} else {
int u, k;
read(u), read(k);
w[u] = k;
splay(u);
}
}
return 0;
}