动态树杂谈

摘要

动态树,一类用来维护森林连通性的数据结构,主要使用Splay来维护偏爱点/边(Preferred child/edge),并且通过点在不同Splay中的移动提取路径,或者是修改父子关系以连接或断开树边

动态树的构建

动态树由于其需要动态的修改点/边关系,所以需要动态的维护点/边,从而选择偏爱点/边而不是像树链剖分中使用轻重点/边,同时需要Spaly维护

每个Splay中保存着一条偏爱路径,从浅到深维护深度
因为在同一棵树中,路径是有交点的,不同的Splay通过这些交点联系起来

不想多说,看代码注释

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 300010;
#define L ch[x][0]
#define R ch[x][1]
struct LCT {
    int ch[N][2] , f[N] , rev[N] , val[N] , x0r[N];
    int sta[N];
    bool get(int x) {return ch[f[x]][1] == x;}
    void update(int x) {x0r[x] = x0r[L] ^ x0r[R] ^ val[x];}
    void pushdown(int x) {if(rev[x]) {rev[x] ^= 1; rev[L] ^= 1; rev[R] ^= 1; swap(L , R);}}
    bool isroot(int x) {return ch[f[x]][0] != x && ch[f[x]][1] != x;}
    void rotate(int x) {
        int old = f[x] , oldf = f[old] , w = get(x);
        if(!isroot(old)) ch[oldf][ch[oldf][1] == old] = x;
        ch[old][w] = ch[x][w ^ 1]; f[ch[old][w]] = old;
        ch[x][w ^ 1] = old; f[old] = x;
        f[x] = oldf;
        update(old); update(x);
    }
    void splay(int x) {
    //因为每次从下层splay取出一个点加入上层节点,这个点必然是深度最小节点,新根必然是深度次小节点,也就是提升上去节点的儿子
    //对于非根节点来说,其f指splay中的f,而对于根节点来说,f不指splay中的f,而是指联系上层splay的节点
    //每次splay其他节点就会将这个联系上层splay的f转移给新的根节点,但是最终要提升是会将深度最小节点作为根节点再进行提升,也就是直接提升为f的儿子
    //所以可以说整个对于同一棵树形成的splay森林,其实就是用虚儿子强行隔开的一个完整的spaly
        int fa = x , top = 0;
        sta[++ top] = fa;
        while(!isroot(fa)) {sta[++top] = fa = f[fa];}
        while(top) pushdown(sta[top --]);
        //从上向下传递所有标记,防止剩余
        for(int fa ; !isroot(x) ; rotate(x)) {
            if(!isroot(fa = f[x]))
                rotate(get(x) == get(fa) ? fa : x);
        }
    }
    void access(int x) {
    //若x与其父亲不在一棵splay中,显然x为深度最小节点,那么将其旋至splay根,并且和其父亲建立联系,抛弃深度比它大的节点,其父亲肯定为深度最大节点,直接接在其右侧,成为上层splay的深度最深节点
    //若x与其父亲在一棵splay中,那么抛弃掉比x深的节点,然后将其父亲旋转至splay根时必然相邻,不会抛弃其他节点
    //最终深度最深节点仍然是x
    //这也是因为其实只有第一次才抛弃了节点,其他时刻并没有抛弃节点
    //创建出一条x-root的通路,x作为深度最深的节点应该在整棵splay的最右侧
        for(int i = 0 ; x ; i = x , x = f[x]) {
            splay(x);
            R = i;
            update(x);
        }
    }
    void makeroot(int x) {
        access(x);
        splay(x);
        rev[x] ^= 1;//最深节点变为最浅节点(根)
    }
    int find(int x) {
        access(x);
        splay(x);
        while(ch[x][0]) x = ch[x][0];
        return x;//寻找x这棵树的最浅节点(根)
    }
    void split(int x , int y) {//创建y->x(rt),并且将y旋转至根
        makeroot(x);
        access(y);
        splay(y);
    }
    void cut(int x , int y) {
        split(x , y);//显然x深度较小(根),如果有路径x<->y,那么说明dep[y] = dep[x] + 1,x,y两者中序遍历应该相邻,即ch[y][0] = x , ch[x][1] = 0;
        if(ch[x][1] == 0 && ch[y][0] == x) {
            ch[y][0] = 0;
            f[x] = 0;
        }
    }
    void link(int x , int y) {
    //最浅节点挂上父亲,因为保证原来没有相连,所以本来根节点的f应该是0
        makeroot(x);
        f[x] = y;
    }
}lct;
int read() {
    int ans = 0 , flag = 1;
    char ch = getchar();
    while(ch > '9' || ch < '0') {if(ch == '-') flag = -1; ch = getchar();}
    while(ch >= '0' &&ch <= '9') {ans = ans * 10 + ch - '0'; ch = getchar();}
    return ans * flag;
}
int main() {
    int n = read() , m = read();
    for(int i = 1 ; i <= n ; ++ i) lct.val[i] = lct.x0r[i] = read();
    while(m --) {
        int opt = read();
        switch (opt) {
            case 0 : {
                int x = read() , y = read();
                lct.split(x , y);
                printf("%d\n" , lct.x0r[y]);
                break;
            }
            case 1 : {
                int x = read() , fx = lct.find(x) , y = read() , fy = lct.find(y);
                //如果没有删边操作尽量使用并查集维护
                if(fx != fy) lct.link(x , y);
                break;
            }
            case 2 : {
                int x = read() , fx = lct.find(x) , y = read() , fy = lct.find(y);
                if(fx == fy) lct.cut(x , y);
                break;
            }
            case 3 : {
                int x = read() , y = read();
                lct.access(x); lct.splay(x);
                lct.val[x] = y;
                lct.update(x);
                break;
            }
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值