Link/Cut Tree学习笔记

最近正是实验课的高峰期,我数了一下,除了毛概没有实验课,其他的课都有实验课。。。不过好在这些实验都不是很难。我尽力挤出时间用来刷题。

简介

Link/Cut Tree和树链剖分很相似,二者处理的问题也有重叠。区别在于后者用线段树维护树链,所以树链是静态的,剖分方式是重链剖分;后者是用Splay维护树链,是动态的,剖分方式是实链剖分,所以Link/Cut Tree有时也被称为动态树(它只是动态树的一种)。

树链剖分的剖分方式结果是由子树决定的,即由题目中给出的数据决定的;而Link/Cut Tree的剖分方式是我们自己选择的,不受题目的限制,且受益于Splay的灵活性,我们可以随意改动树的结构。以此实现树链的 l i n k link link c u t cut cut 操作。

Link/Cut Tree维护的是一个森林,每棵树是一个Splay树,Splay树内的边为实边,树与树之间有一些虚边连接。实边就是双向边,即儿子节点指向父亲节点,父亲节点也指向儿子节点,而虚边是单向边,儿子节点指向父亲节点,父亲节点不指向儿子节点。

性质

  • 每一个Splay的中序遍历序列的点,对应在原树中的深度严格递增
  • 每个节点被包含且仅被包含于一个Splay中。
  • 因为性质2,当某点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个Splay中的。那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的Splay的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。

有一些容易混淆的概念。

原树是指题目给出的树,我们在原树里按照上述性质划分各个实边和虚边。

划分完成后,由实边组成的联通块组成一个Splay,每一个Splay的结构与他们在原树中的位置无关,Splay的结构只受限于“每一个Splay的中序遍历序列的点,对应在原树中的深度严格递增”。

结构

#define lc T[x][0]
#define rc T[x][1]
ch[N][2]:左右儿子
f[N]:父亲指向
tag[N]:翻转标记
laz[N]:权值标记
siz[N]:辅助树上子树大小

基本操作

PushUp

更新一下子树的大小,在节点位置发生变化时调用。

inline void pushup(register int x) {
    
      //上传信息
    siz[x] = siz[lc] + siz[rc];
}

PushDown

释放懒标记,向下移动时调用。

inline void pushr(register int x) {
    
    
    swap(lc, rc);
    tag[x] ^= 1;
}  //翻转操作
inline void pushdown(register int x) {
    
      //判断并释放懒标记
    if (tag[x]) {
    
    
        if (lc) pushr(lc);
        if (rc) pushr(rc);
        tag[x] = 0;
    }
}

isRoot

判断节点是否为一个Splay的根(与普通Splay的区别1)。

是返回1,否则返回0.

inline bool nroot(register int x) {
    
      //判断节点是否为一个Splay的根(与普通Splay的区别1)
    return T[f[x]][0] == x || T[f[x]][1] == x;
}  //原理很简单,如果连的是轻边,他的父亲的儿子里没有它

Splay && Rotate

Splay中的基础操作。

Splay()只有一个参数,表示将 x x x 移动到根节点。

inline void rotate(register int x) {
    
      //一次旋转
    register int y = f[x], z = f[y], k = T[y][1] == x, w = T[x][!k];
    if (nroot(y)) T[z][T[z][1] == y] = x;
    T[x][!k] = y;
    T[y][k] = w;  //额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    if (w) f[w] = y;
    f[y] = x;
    f[x] = z;
    pushup(y);
}
inline void splay(
    register int x) {
    
      //只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
    register int y = x, z = 0;
    st[++z] = y;  // st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
    while (nroot(y)) st[++z] = y = f[y];
    while (z) pushdown(st[z--]);
    while (nroot(x)) {
    
    
        y = f[x];
        z = f[y];
        if (nroot(y)) rotate((T[y][0] == x) ^ (T[z][0] == y) ? x : y);
        rotate(x);
    }
    pushup(x);
}

Access

Link/Cut Tree核心操作,将根节点到 x x x 的路径点划分到一个Splay中。

  • 把当前节点转到根。
  • 把儿子换成之前的节点。
  • 更新当前点的信息。
  • 把当前点换成当前点的父亲,继续操作。

我们有这样一棵树,实线为实边,虚线为虚边。

它的辅助树可能长成这样(构图方式不同可能 LCT 的结构也不同)。

每个绿框里是一棵 Splay。

现在我们要 A c c e s s ( N ) Access(N) Access(N) , 把 A A A N N N 路径上的边都变为实边,拉成一棵 Splay。

实现的方法是从下到上逐步更新 Splay。

首先我们要把 N N N 旋至当前 Splay 的根。

为了保证 AuxTree(辅助树)的性质,原来 N N N O O O 的实边要更改为虚边。

由于认父不认子的性质,我们可以单方面的把 N N N 的儿子改为 Null。

于是原来的 AuxTree 就从下图变成了下下图。

下一步,我们把 N N N 指向的 F a [ I ] Fa[I] Fa[I] 也旋转到 I I I 的 Splay 树根。

原来的实边 I − K I - K IK 要去掉,这时候我们把 I I I 的右儿子指向 N N N , 就得到了 I − L I - L IL 这样一棵 Splay。

接下来,按照刚刚的操作步骤,由于 I I I F a [ H ] Fa[H] Fa[H] , 我们把 H H H 旋转到他所在 Splay Tree 的根,然后把 H H H r s rs rs 设为 I I I

之后的树是这样的。

同理我们 Splay(A) , 并把 A A A 的右儿子指向 H H H

于是我们得到了这样一棵 AuxTree。并且发现 A − H A - H AH 的整个路径已经在同一棵 Splay 中了。大功告成!

inline void access(register int x) {
    
      //访问
    for (register int y = 0; x; x = f[y = x]) splay(x), rc = y, pushup(x);
}

makeRoot

把节点 x x x 成为原树的根节点。

inline void makeroot(register int x) {
    
      //换根
    access(x);
    splay(x);
    pushr(x);
}

FindRoot

找到 x x x 在原树中的根,判断两点连通性的时候会用到。

a c c e s s ( x ) access(x) access(x),那么x就和根节点在同一Splay中了。然后 s p l a y ( x ) splay(x) splay(x),那么 x x x 就成了Splay的根(Splay的根和原树的根不是一个概念)。

由于Splay符合二叉搜索树的性质,而且根节点的深度一定最小,所以只需要从 x x x 以置向左走,就能最后走到原树的根节点。

最后将找到的原树根节点 s p l a y splay splay 一下,以防被卡。

int findroot(register int x) {
    
      //找根(在真实的树中的)
    access(x);
    splay(x);
    while (lc) pushdown(x), x = lc;
    splay(x);
    return x;
}

Link

连一条 x − y x - y xy 的边。此处使 x x x 的父亲指向 y。

连边之前需要判两点是否已经联通。先把 x x x 置为原树的根节点,如果 x x x y y y 已经联通那么在 y y y 的位置查询到的根节点一定就是 x x x ,否则 x x x y y y 未联通。

我们在这里虽然改变了原树的根节点,但是原树的结构是没有发生变化的,这和Splay的 s p l a y splay splay 操作不同。

inline void link(register int x, register int y) {
    
      //连边
    makeroot(x);
    if (findroot(y) != x) f[x] = y;
}

Split

提取出原树中 x x x 点到 y y y 点的路径。

先把 x x x 点设置为原树的根,然后 a c c e s s ( y ) access(y) access(y) 即可。此时 x x x 为根,即深度最小的节点, y y y 没有右儿子,即 y y y 是当前 Splay 中深度最大的节点。那么中序遍历的序列就是 x x x 开头, y y y 结尾。

最后 s p l a y splay splay 一下,防止被卡。

inline void split(register int x, register int y) {
    
      //提取路径
    makeroot(x);
    access(y);
    splay(y);
}

Cut

我认为对初学者来说这个有点难以理解。

首先需要检查 x x x y y y 是否已经联通。如果联通了且两点相邻,则断开这条边;否则两点不联通,不用执行任何操作。

先把 x x x 置为原树的根结点,然后查询 y y y 的根结点是否为 x x x 。是的话则 x x x y y y 此时在同一个Splay,即两点在原树中已经联通。

如果 x x x y y y 联通,那么需要判断 x x x y y y 在原树中是否相邻。由于前面把 x x x 置为原树的根结点,所以此时 x x x 的深度最小(没有左儿子),那么 y y y 一定在 x x x 的右儿子的子树里。

那么如果 x x x y y y 在原树中相邻,中序遍历这颗Splay后, x x x y y y 在序列里的位置一定相邻(中序遍历一颗Splay,得到的序列的点在原树中的深度递增,所以路径在原树中是一颗向下的链,且没有分支)。那么 y y y 的父亲节点一定是 x x x y y y 没有左儿子。如果 y y y 有左儿子,那么中序遍历时访问 x x x 后会先访问 y y y 的左儿子, x x x y y y 在序列里不相邻。

断边时双向断边。

inline void cut(register int x, register int y) {
    
      //断边
    makeroot(x);
    if (findroot(y) == x && f[y] == x && !T[y][0]) {
    
    
        f[y] = T[x][1] = 0;
        pushup(x);
    }
}

模板

#define lc T[x][0]
#define rc T[x][1]
int f[maxn], T[maxn][2], siz[maxn], st[maxn];
bool tag[maxn];
inline bool nroot(register int x) {
    
      //判断节点是否为一个Splay的根(与普通Splay的区别1)
    return T[f[x]][0] == x || T[f[x]][1] == x;
}  //原理很简单,如果连的是轻边,他的父亲的儿子里没有它
inline void pushup(register int x) {
    
      //上传信息
    siz[x] = siz[lc] + siz[rc];
}
inline void pushr(register int x) {
    
    
    swap(lc, rc);
    tag[x] ^= 1;
}  //翻转操作
inline void pushdown(register int x) {
    
      //判断并释放懒标记
    if (tag[x]) {
    
    
        if (lc) pushr(lc);
        if (rc) pushr(rc);
        tag[x] = 0;
    }
}
inline void rotate(register int x) {
    
      //一次旋转
    register int y = f[x], z = f[y], k = T[y][1] == x, w = T[x][!k];
    if (nroot(y)) T[z][T[z][1] == y] = x;
    T[x][!k] = y;
    T[y][k] = w;  //额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    if (w) f[w] = y;
    f[y] = x;
    f[x] = z;
    pushup(y);
}
inline void splay(
    register int x) {
    
      //只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
    register int y = x, z = 0;
    st[++z] = y;  // st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
    while (nroot(y)) st[++z] = y = f[y];
    while (z) pushdown(st[z--]);
    while (nroot(x)) {
    
    
        y = f[x];
        z = f[y];
        if (nroot(y)) rotate((T[y][0] == x) ^ (T[z][0] == y) ? x : y);
        rotate(x);
    }
    pushup(x);
}
/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
inline void pushall(register int x){
        if(nroot(x))pushall(f[x]);
        pushdown(x);
}*/
inline void access(register int x) {
    
      //访问
    for (register int y = 0; x; x = f[y = x]) splay(x), rc = y, pushup(x);
}
inline void makeroot(register int x) {
    
      //换根
    access(x);
    splay(x);
    pushr(x);
}
int findroot(register int x) {
    
      //找根(在真实的树中的)
    access(x);
    splay(x);
    while (lc) pushdown(x), x = lc;
    splay(x);
    return x;
}
inline void split(register int x, register int y) {
    
      //提取路径
    makeroot(x);
    access(y);
    splay(y);
}
inline void link(register int x, register int y) {
    
      //连边
    makeroot(x);
    if (findroot(y) != x) f[x] = y;
}
inline void cut(register int x, register int y) {
    
      //断边
    makeroot(x);
    if (findroot(y) == x && f[y] == x && !T[y][0]) {
    
    
        f[y] = T[x][1] = 0;
        pushup(x);
    }
}

模板题

洛谷 P3690

代码

// #pragma GCC optimize(2)
#include <bits/stdc++.h>
#define m_p make_pair
#define p_i pair<int, int>
#define _for(i, a) for(register int i = 0, lennn = (a); i < lennn; ++i)
#define _rep(i, a, b) for(register int i = (a), lennn = (b); i <= lennn; ++i)
#define outval(a) cout << "Debuging...|" << #a << ": " << a << "\n"
#define mem(a, b) memset(a, b, sizeof(a))
#define mem0(a) memset(a, 0, sizeof(a))
#define fil(a, b) fill(a.begin(), a.end(), b);
#define scl(x) scanf("%lld", &x)
#define sc(x) scanf("%d", &x)
#define pf(x) printf("%d\n", x)
#define pfl(x) printf("%lld\n", x)
#define abs(x) ((x) > 0 ? (x) : -(x))
#define PI acos(-1)
#define lowbit(x) (x & (-x))
#define dg if(debug)
#define nl(i, n) (i == n - 1 ? "\n":" ")
using namespace std;
typedef long long LL;
// typedef __int128 LL;
typedef unsigned long long ULL;
const int maxn = 100005;
const int maxm = 1000005;
const int maxp = 30;
const int inf = 0x3f3f3f3f;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1000000007;
const double eps = 1e-8;
const double e = 2.718281828;
int debug = 0;

inline int read() {
    
    
    int x(0), f(1); char ch(getchar());
    while (ch<'0' || ch>'9') {
    
     if (ch == '-') f = -1; ch = getchar(); }
    while (ch >= '0'&&ch <= '9') {
    
     x = x * 10 + ch - '0'; ch = getchar(); }
    return x * f;
}

int v[maxn];

#define lc T[x][0]
#define rc T[x][1]
int f[maxn], T[maxn][2], val[maxn], st[maxn];
bool tag[maxn];
inline bool nroot(register int x) {
    
      //判断节点是否为一个Splay的根(与普通Splay的区别1)
    return T[f[x]][0] == x || T[f[x]][1] == x;
}  //原理很简单,如果连的是轻边,他的父亲的儿子里没有它
inline void pushup(register int x) {
    
      //上传信息
    val[x] = val[lc] ^ val[rc] ^ v[x];
}
inline void pushr(register int x) {
    
    
    swap(lc, rc);
    tag[x] ^= 1;
}  //翻转操作
inline void pushdown(register int x) {
    
      //判断并释放懒标记
    if (tag[x]) {
    
    
        if (lc) pushr(lc);
        if (rc) pushr(rc);
        tag[x] = 0;
    }
}
inline void rotate(register int x) {
    
      //一次旋转
    register int y = f[x], z = f[y], k = T[y][1] == x, w = T[x][!k];
    if (nroot(y)) T[z][T[z][1] == y] = x;
    T[x][!k] = y;
    // pushup(x);
    T[y][k] = w;  //额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    if (w) f[w] = y;
    f[y] = x;
    f[x] = z;
    pushup(y);
}
inline void splay(register int x) {
    
    
    register int y = x, z = 0;
    st[++z] = y;  // st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
    while (nroot(y)) st[++z] = y = f[y];
    while (z) pushdown(st[z--]);
    while (nroot(x)) {
    
    
        y = f[x];
        z = f[y];
        if (nroot(y)) rotate((T[y][0] == x) ^ (T[z][0] == y) ? x : y);
        rotate(x);
    }
    pushup(x);
}
/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
inline void pushall(register int x){
        if(nroot(x))pushall(f[x]);
        pushdown(x);
}*/
inline void access(register int x) {
    
      //访问
    for (register int y = 0; x; x = f[y = x]) splay(x), rc = y, pushup(x);
}
inline void makeroot(register int x) {
    
      //换根
    access(x);
    splay(x);
    pushr(x);
}
int findroot(register int x) {
    
      //找根(在真实的树中的)
    access(x);
    splay(x);
    while (lc) pushdown(x), x = lc;
    splay(x);
    return x;
}
inline void split(register int x, register int y) {
    
      //提取路径
    makeroot(x);
    access(y);
    splay(y);
}
inline void link(register int x, register int y) {
    
      //连边
    makeroot(x);
    if (findroot(y) != x) f[x] = y;
}
inline void cut(register int x, register int y) {
    
      //断边
    makeroot(x);
    if (findroot(y) == x && f[y] == x && !T[y][0]) {
    
    
        f[y] = T[x][1] = 0;
        pushup(x);
    }
}

int main() {
    
    
    //ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
#ifdef ONLINE_JUDGE
#else
    freopen("in.txt", "r", stdin);
    debug = 1;
#endif
    time_t beg, end;
    if(debug) beg = clock();

    int n = read(), m = read();
    _rep(i, 1, n) v[i] = read();
    _for(i, m) {
    
    
        int op = read(), x = read(), y = read();
        if(op == 0) split(x, y), printf("%d\n", val[y]);
        else if(op == 1) link(x, y);
        else if(op == 2) cut(x, y);
        else if(op == 3) v[x] = y, splay(x);
    }

    if(debug) {
    
    
        end = clock();
        printf("time:%.2fs\n", 1.0 * (end - beg) / CLOCKS_PER_SEC);
    }
    return 0;
}

参考:
OI Wiki
博客园-FlashHu

猜你喜欢

转载自blog.csdn.net/weixin_42856843/article/details/109330709