2022年02月01日,第十四天
前言:以下笔记是参考博客:LCT总结——概念篇 (侵删)
一、概念性质
链剖分,是指一类对树的边进行轻重划分的操作,这样做的目的是为了减少某些链上的修改、查询等操作的复杂度。目前总共有三类:重链剖分,实链剖分和不常见的长链剖分。
- 重链剖分,实际上我们经常讲到树剖,就是重链剖分的常用称呼。对于每个点,选择最大的子树,将这条边划分为重边,而连向其它子树的边划分为轻边。若干重边连接在一起构成重链,用树状数组或线段树等数据结构维护。
- 实链剖分,同样将某一个儿子的连边划分为实边,而连向其它子树的边划分为虚边。区别在于虚实是可以动态变化的,因此要使用更高级、更灵活的
S
p
l
a
y
Splay
Splay 来维护每一条由若干实边连接而成的实链。基于性质更加优秀的实链剖分,
L
C
T
LCT
LCT 应运而生。
L
C
T
LCT
LCT 维护的对象其实是一个森林。在实链剖分的基础下,
L
C
T
LCT
LCT 支持更多的操作。
- 查询、修改链上的信息(最值、总和等)
- 随意指定原树的根(即换根)
- 动态连边、删边
- 合并两棵树、分离一棵树
- 动态维护连通性
L
C
T
LCT
LCT 的主要性质如下:
- 每一个
S
p
l
a
y
Splay
Splay 维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历
S
p
l
a
y
Splay
Splay 得到的每个点的深度序列严格递增。
- 每个结点包含且仅包含于一个
S
p
l
a
y
Splay
Splay 中。
- 边分为实边和虚边,实边包含在
S
p
l
a
y
Splay
Splay 中,而虚边总是由一颗
S
p
l
a
y
Splay
Splay 指向另一个结点(指向该
S
p
l
a
y
Splay
Splay 中中序遍历最靠前的点在原树中的父亲)。
由以上性质,当某个结点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个
S
p
l
a
y
Splay
Splay 中的。那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的
S
p
l
a
y
Splay
Splay 的根结点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。
二、各种操作
1.
a
c
c
e
s
s
(
x
)
access(x)
access(x)
L
C
T
LCT
LCT 核心操作,也是最难理解的操作。其它所有的操作都是在此基础上完成的。因为性质
3
3
3 ,我们不能总是保证每两个点之间的路径是直接连通的(在一个
S
p
l
a
y
Splay
Splay 上)。
a
c
c
e
s
s
access
access 定义为打通根结点到指定结点的实链,使得一条中序遍历以根开始、以指定点结束的
S
p
l
a
y
Splay
Splay 出现。
void access (int x) {
int z = x;
for (int y = 0; x; y = x, x = t[x].fa) {
splay (x);
t[x].ch[1] = y;
pushup (x);
}
splay (z);
}
2.
m
a
k
e
r
o
o
t
(
x
)
makeroot(x)
makeroot(x)
只是把根到某个结点的路径打通并不能满足我们的需求。更多时候,我们要获取指定两个结点之间的路径信息。然而可能这两个点不是祖孙关系(该路径不能满足按深度严格递增的要求)。根据性质
1
1
1 ,这样的路径不能在一个
S
p
l
a
y
Splay
Splay 中。
m
a
k
e
r
o
o
t
makeroot
makeroot 定义为换根,让指定点成为原树的根。这时候就利用到
a
c
c
e
s
s
(
x
)
access(x)
access(x) 和
S
p
l
a
y
Splay
Splay 的翻转操作。
a
c
c
e
s
s
(
x
)
access(x)
access(x) 后
x
x
x 一定是
S
p
l
a
y
Splay
Splay 中,中序遍历最后的点。
S
p
l
a
y
(
x
)
Splay(x)
Splay(x) 后,
x
x
x 在
S
p
l
a
y
Splay
Splay 中将没有右子树(性质
1
1
1 )。于是翻转整个
S
p
l
a
y
Splay
Splay ,使得所有点的深度都倒过来了,
x
x
x 没了左子树,反倒成了深度最小的点(根结点),达到了我们的目的。
void pushrev (int x) {
swap (t[x].ch[0], t[x].ch[1]);
t[x].rev ^= 1;
}
void makeroot (int x) {
access (x);
pushrev (x);
}
3.
f
i
n
d
r
o
o
t
(
x
)
findroot(x)
findroot(x)
找
x
x
x 所在原树的树根,主要用来判断两点之间的连通性(
f
i
n
d
r
o
o
t
(
x
)
=
f
i
n
d
r
o
o
t
(
y
)
findroot(x) =findroot(y)
findroot(x)=findroot(y) 表示
x
,
y
x,y
x,y 在同一颗树中)。利用性质
1
1
1 ,不停找左儿子,因为其深度一定比当前点深度小。
int findroot (int x) {
access (x);
while (t[x].ch[0]) pushdown (x), x = t[x].ch[0];
splay (x);
return x;
}
4.
s
p
l
i
t
(
x
,
y
)
split(x,y)
split(x,y)
神奇的
m
a
k
e
r
o
o
t
makeroot
makeroot 已经出现,我们可以访问指定的一条在原树中的链。
s
p
l
i
t
(
x
,
y
)
split(x,y)
split(x,y) 定义为打通一条
x
x
x 到
y
y
y 的路径成为一个
S
p
l
a
y
Splay
Splay ,最终以
y
y
y 作为该
S
p
l
a
y
Splay
Splay 的根,
x
x
x 成为了根,那么
x
x
x 到
y
y
y 的路径就可以用
a
c
c
e
s
s
(
y
)
access(y)
access(y) 直接打通出来了,将
y
y
y 转到
S
p
l
a
y
Splay
Splay 根后,我们就可以直接通过访问
y
y
y 来获取该路径的有关信息。
void split (int x, int y) {
makeroot (x);
access (y), split(y);
}
5.
l
i
n
k
(
x
,
y
)
link(x, y)
link(x,y)
连一条
x
x
x 到
y
y
y 的边(使得
x
x
x 的父亲指向
y
y
y ,连一条轻边)
void link (int x, int y) {
makeroot (x);
if (findroot (y) != x) t[x].fa = y;
}
6.
c
u
t
(
x
,
y
)
cut(x, y)
cut(x,y)
当不一定存在该边时,我们要考虑三个条件
- 判断一下连通性(注意
f
i
n
d
r
o
o
t
(
y
)
findroot(y)
findroot(y) 以后
x
x
x 成了根);
- 再看看
x
,
y
x,y
x,y 是否有父子关系;
- 还要看
y
y
y 是否有左儿子。
原因如下:
a
c
c
e
s
s
(
y
)
access(y)
access(y) 以后,假如
y
y
y 与
x
x
x 在同一
S
p
l
a
y
Splay
Splay 中而没有连边。
- 那么这条路径上一定会有其它点,在中序遍历序列中的位置介于
x
x
x 与
y
y
y 之间;
- 那么可能
y
y
y 的父亲就不是
x
x
x 了;
- 那么可能
y
y
y 的父亲还是
x
x
x ,其它的点就在
y
y
y 的左子树中。
void cut (int x, int y) {
makeroot (x);
if (findroot(y) == x && t[y].fa == x && ! t[y].ch[0]) {
t[x].ch[1] = t[y].fa = 0;
pushup (x);
}
}
7.
i
s
r
o
o
t
(
x
)
isroot(x)
isroot(x)
用于判断
x
x
x 是否是当前
S
p
l
a
y
Splay
Splay 的根。当
x
x
x 没有父亲的孩子连边时满足条件。
bool isroot (int x) {
return t[t[x].fa].ch[0] != x && t[t[x].fa].ch[1] != x;
}
#include <bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define re register
#define endl '\n'
using namespace std;
const int MOD = 998244353;
const int inf = 0x3f3f3f3f;
const double eps = 1e-8;
const double Pi = acos(-1.0);
const int N = 1e5 + 5;
int n, m;
struct node {
int ch[2], fa, val;
int sum, rev;
}t[N];
int st[N];
inline int ident (int x, int f) { return t[f].ch[1] == x; }
inline void connect (int x, int f, int s) {
t[x].fa = f, t[f].ch[s] = x;
}
void pushrev (int x) {
swap (t[x].ch[0], t[x].ch[1]);
t[x].rev ^= 1;
}
void pushup (int x) {
t[x].sum = t[t[x].ch[0]].sum ^ t[x].val ^ t[t[x].ch[1]].sum;
}
void pushdown (int x) {
if (t[x].rev) {
pushrev (t[x].ch[0]);
pushrev (t[x].ch[1]);
t[x].rev = 0;
}
}
bool isroot (int x) {
return t[t[x].fa].ch[0] != x && t[t[x].fa].ch[1] != x;
}
void rotate (int x) {
int f = t[x].fa, ff = t[f].fa, k = ident (x, f);
if (! isroot (f))
t[ff].ch[ident (f, ff)] = x;
t[x].fa = ff;
connect (t[x].ch[k ^ 1], f, k);
connect (f, x, k ^ 1);
pushup (f), pushup (x);
}
void splay (int x) {
int top = 0, r = x;
st[++ top] = r;
while (! isroot (r)) st[++ top] = r = t[r].fa;
while (top) pushdown (st[top --]);
while (! isroot (x)) {
int f = t[x].fa, ff = t[f].fa;
if (! isroot (f))
ident (x, f) ^ ident (f, ff) ? rotate (x) : rotate (f);
rotate (x);
}
}
void access (int x) {
int z = x;
for (int y = 0; x; y = x, x = t[x].fa) {
splay (x);
t[x].ch[1] = y;
pushup (x);
}
splay (z);
}
void makeroot (int x) {
access (x);
pushrev (x);
}
int findroot (int x) {
access (x);
while (t[x].ch[0]) pushdown (x), x = t[x].ch[0];
splay (x);
return x;
}
void split (int x, int y) {
makeroot (x);
access (y), splay (y);
}
void link (int x, int y) {
makeroot (x);
if (findroot (y) != x) t[x].fa = y;
}
void cut (int x, int y) {
makeroot (x);
if (findroot(y) == x && t[y].fa == x && ! t[y].ch[0]) {
t[x].ch[1] = t[y].fa = 0;
pushup (x);
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i ++)
cin >> t[i].val;
while (m --) {
int op, x, y;
cin >> op >> x >> y;
if (op == 0) {
split (x, y);
cout << t[y].sum << endl;
}else if (op == 1) link (x, y);
else if (op == 2) cut (x, y);
else {
splay (x);
t[x].val = y;
pushup (x);
}
}
return 0;
}
|