初识动态树—— Link Cut Tree

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 出现。

//建立一条从根结点到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);
}
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 没了左子树,反倒成了深度最小的点(根结点),达到了我们的目的。

// 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);
}
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 ,不停找左儿子,因为其深度一定比当前点深度小。

// 找到x所在原树的根结点,再将原树的根结点旋转成splay的跟结点
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 来获取该路径的有关信息。

// 给x和y之间的路径建立一个splay,其根结点是y
void split (int x, int y) { 
    makeroot (x);
    access (y), splay(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 ,连一条轻边)

// 如果x和y不连通,则加入一条x和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 的左子树中。
// 如果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);
    }
}
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;
}

三、一道模板题:P3690 【模板】动态树(Link Cut Tree)

#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;
}

你可能感兴趣的:(数据结构,数据结构,算法)