2022年02月01日,第十四天
前言:以下笔记是参考博客:LCT总结——概念篇 (侵删)
链剖分,是指一类对树的边进行轻重划分的操作,这样做的目的是为了减少某些链上的修改、查询等操作的复杂度。目前总共有三类:重链剖分,实链剖分和不常见的长链剖分。
L C T LCT LCT 的主要性质如下:
由以上性质,当某个结点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个 S p l a y Splay Splay 中的。那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的 S p l a y Splay Splay 的根结点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。
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 出现。
//建立一条从根结点到x的路径,同时将x变成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);
}
只是把根到某个结点的路径打通并不能满足我们的需求。更多时候,我们要获取指定两个结点之间的路径信息。然而可能这两个点不是祖孙关系(该路径不能满足按深度严格递增的要求)。根据性质 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 没了左子树,反倒成了深度最小的点(根结点),达到了我们的目的。
// splay 区间翻转操作
void pushrev (int x) {
swap (t[x].ch[0], t[x].ch[1]);
t[x].rev ^= 1; // lazy tag
}
// 将x变成原树的根结点
void makeroot (int x) {
access (x);
pushrev (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 ,不停找左儿子,因为其深度一定比当前点深度小。
// 找到x所在原树的根结点,再将原树的根结点旋转成splay的跟结点
int findroot (int x) {
access (x);
while (t[x].ch[0]) pushdown (x), x = t[x].ch[0];
splay (x); // 保证复杂度
return x;
}
神奇的 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 来获取该路径的有关信息。
// 给x和y之间的路径建立一个splay,其根结点是y
void split (int x, int y) {
makeroot (x);
access (y), splay(y);
}
连一条 x x x 到 y y y 的边(使得 x x x 的父亲指向 y y y ,连一条轻边)
// 如果x和y不连通,则加入一条x和y之间的边
void link (int x, int y) {
makeroot (x);
if (findroot (y) != x) t[x].fa = 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和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);
}
}
用于判断 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
#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) { //建立一条从根结点到x的路径,同时将x变成splay的根结点
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) { // 将x变成原树的根结点
access (x);
pushrev (x);
}
int findroot (int x) { // 找到x所在原树的根结点,再将原树的根结点旋转成splay的跟结点
access (x);
while (t[x].ch[0]) pushdown (x), x = t[x].ch[0];
splay (x);
return x;
}
void split (int x, int y) { // 给x和y之间的路径建立一个splay,其根结点是y
makeroot (x);
access (y), splay (y);
}
void link (int x, int y) { // 如果x和y不连通,则加入一条x和y之间的边
makeroot (x);
if (findroot (y) != x) t[x].fa = y;
}
void cut (int x, int y) { // 如果x和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;
}