• 蒟蒻的平衡树学习笔记(=.=


    蒟蒻的平衡树学习笔记

    动态更新,在整理QWQ

    平衡树主要用来动态维护一些在序列上的操作问题

    通过旋转操作使树平衡,且保持中序遍历结果不变,即左小右大的结构,便于在log的时间内访问节点

    本文主要使用splay, 一种使用范围较为广泛,方便理解(划掉)的数据结构

    (其实还有更简单的,比如说vector+二分,树状数组+二分,map,pbds(noip禁用)等等,但是并不泛用)

    当然,替罪羊树,红黑树,各种treap, 各种树套各种树也不是不行 ( 只是作者菜不会

    下面分步介绍平衡树这一数据结构 ( 较为详细

    1 . 开树的结构体

    树本文一般用结构体表示,较为清晰

    主要有以下几个变量

    struct splay{
        int tag;
        //懒标记
        int cnt;
        //记录某个权值出现的次数
        int sz;
        //记录这个节点以下子树的大小
        int ch[2];
        //ch[0]表示左节点,ch[1]表示右节点
    //这样便于访问(后面会有很大帮助,能减少讨论各种情况,缩短代码长度
    int fa; //记录这个节点的父亲 int val; //记录这个节点的权值 int ******** //根据题目的具体要求自信定义 }T[1001010];

    2.update操作

    //update操作主要用来把左右子节点的大小信息传上去,更新各种旋转操作后子树的大小信息
    //代码很短,意思是当前节点的子树,等于左儿子的子树大小加上右儿子的子树大小
    //再加上自己节点数的个数cnt。
    int update(int x) { T[x].sz = T[T[x].ch[0]].sz + T[T[x].ch[1]].sz + T[x].cnt; }

    细节:没啥细节的,有的题目cnt是1(因为不会出现重复元素),记得改成1就好了(

    3.wh操作

    //wh操作是用来询问当前节点为父亲节点的左节点还是右节点
    bool wh(int x) { return T[T[x].fa].ch[1] == x; }
    //1是右节点,如果x 等于他的父亲的右儿子,即T[T[x].fa].ch[1],等于x就会返回真,也就是1
    //这时说明这个节点的是他父亲的右儿子(废话),否则就是左儿子ch[0]

    细节:也没啥细节的(, 记得一定是ch[1] 就完事了(

    4.pushdown操作

    //pushdown操作主要用来下放懒标记的
    //这里只有取反操作
    void pushdown(int x) {
        if(T[x].tag) {
            T[T[x].ch[0]].tag ^= 1, T[T[x].ch[1]].tag ^= 1, T[x].tag = 0;
            //如果这个节点有懒标记,就把左子树和右子树取反,把原标记赋值为0
            swap (T[x].ch[0], T[x].ch[1]);
            //然后交换左右两个节点
        }
    }

    细节:记得先判是否有tag,最后要清空 (线段树的痛现在别犯了QWQ

    5.rotate操作

    //接下去是最重要的rotate操作了
    //通过旋转操作,既可以维护splay的平衡,同时又不破坏遍历后结果的操作,
    //为了方便画图理解,我们把左右子树称为方向(就是左右两个方向)
    //主要有三步(以下操作建议按照描述手动画个图会比较好理解
    //1.连接这个节点的fa和这个节点所属方向(d1)相反方向的节点,连在这个fa的d1方向上
    //2. 连接这个节点的fa和自己,连在这个节点的d1的相反方向上
    //3. 直接把这个节点连在grandfather上,方向是d2(就是fa的子节点方向
    //因为这样旋转会改变各个节点往下的子树大小啊,所以我们要更新节点的子树大小信息
    void rotate(int x) {
        int fa = T[x].fa, gf = T[fa].fa;
        int d1 = wh(x), d2 = wh(fa);
        cont(T[x].ch[d1 ^ 1], fa, d1);
        cont(fa, x, d1 ^ 1);
        cont(x, gf, d2);
        update(fa), update(x);
    }

    细节1: cont连的顺序可以颠倒,但是一定不能打错,最好背下来(

    细节2:update的时候一定是先更新fa再更新x,这样才不会互相影响

    6 . splay操作

    void splay(int x, int goal) {
        //将x的fa一路旋转到goal 
            while (T[x].fa != goal) {
            int fa = T[x].fa, gf = T[fa].fa;
            if (gf != goal)
                (wh(x) == wh(fa)) ? rotate(fa) : rotate(x);
                    //如果x和fa方向相同,就旋转fa,否则只能旋转x
            rotate(x);
                    //旋转到gf等于goal的时候,再把x旋转上去
        }
        if (! goal) root = x;
            //记得特判, 没有父亲的就是根(无父即为王)
    }

     细节:记得特判

     7.inset操作

    //这个insert操作比较好理解
    //cnt是记录元素出现个数的,然后先一路根据左小右大关系while到目标的x
    //如果这个数已经存在了,就直接计数器++,否则新开一个节点
    //(有时候这里x是等于rubbish(),rubbish函数是用来给x分配新节点的,后面会讲(一般空间够的话就用不到) //然后创建一个新节点 void insert(int v) { int x = root, fa = 0; while (x && T[x].val != v) fa = x, x = T[x].ch[T[x].val < v]; if (x) T[x].cnt ++; else { x = ++ cnt; if (fa) T[fa].ch[T[fa].val < v] = x; //特判fa是根的情况 T[x].fa = fa, T[x].sz = 1, T[x].cnt = 1, T[x].val = v; } splay(x, 0); //记得把x转上去!!否则一直插可能插出一条链,树就退化了,这里打错可能不会错,但是会莫名其妙t飞!!! }

    8.find函数

    //find函数主要用来找到权值为v的元素的位置,返回并输出
    //代码类似前面的insert函数
    int find(int v) {
        int x = root;
        while (T[x].val != v && T[x].ch[T[x].val < v])
            x = T[x].ch[T[x].val < v];
        return x;
    }

    9.getrank函数

    //find该节点,旋转到根后输出左子树的权值就好 
    int getrank(int x) {
        splay(find(x),0);
        return T[T[root].ch[0]].sz;
    }

    10.getpre函数

    
    
    //k == 0 找前驱,k == 1 找后继 
    //将该节点旋转到根后返回左子树最右边的节点,否则返回右子树最左边的节点
    int getpre(int v, int k) {
        splay(find(v), 0);
        //将最大小于等于v的节点splay到根 
        int x = root;
        if (!k && T[x].val < v) return x;
        if (k && T[x].val > v) return x;
        x = T[x].ch[k];
        while (T[x].ch[k ^ 1]) x = T[x].ch[k ^ 1];
        return x;
    }

     11.删除节点

    //删除节点
    //记下当前节点的前驱和后继
    //把lt翻到根,把后继也翻上去
    //把计数器减减后再翻上去
    void del(int v) {
        int lt = getpre(v, 0);
        int ne = getpre(v, 1);
        splay (lt, 0), splay (ne, lt);
        if (T[T[ne].ch[0]].cnt > 1) T[T[ne].ch[0]].cnt --, splay(T[ne].ch[0],0);
        else T[ne].ch[0] = 0;
    }

    细节:把前驱翻上去后,这是后继应该接到前驱上,而非根上

     12.根据排名找权值

    //从根开始遍历,往下递归
    //记得先pushdown更新,pushdown后的才是准确的值
    //因为左小右大,所以每次如果子树比排名小的话,扣掉左子树大小+当前节点计数器,否则再往左儿子走,直到子树大小比排名小
    //最后当rk==0的时候,返回x的权值就好了
    int getnum(int rk) {
        int x = root;
        while (1) {
              pushdown(x);
              int ls = T[x].ch[0], rs = T[x].ch[1]; 
              if (rk <= T[ls].sz) x = ls;
              else if (rk > T[ls].sz + T[x].cnt) rk -= T[ls].sz + T[x].cnt, x = rs;
            else return T[x].val;
        }
    }

    接下去就是喜闻乐见的完整代码了(

    //普通平衡树,完整代码
    #include<bits/stdc++.h> #define rqtql cout << "rqtql" << endl using namespace std; const int inf = 2147483647; struct st { int tag, cnt, sz, ch[2], fa, val; }T[1001010]; int root, cnt, n; int update(int x) { T[x].sz = T[T[x].ch[0]].sz + T[T[x].ch[1]].sz + T[x].cnt; } bool wh(int x) { return T[T[x].fa].ch[1] == x; } void cont(int x, int fa, int h) { T[fa].ch[h] = x, T[x].fa = fa; } void pushdown(int x) { if(T[x].tag) { T[T[x].ch[0]].tag ^= 1, T[T[x].ch[1]].tag ^= 1, T[x].tag = 0; swap (T[x].ch[0], T[x].ch[1]); } } void rotate(int x) { int fa = T[x].fa, gf = T[fa].fa; int s1 = wh(x), s2 = wh(fa); cont(T[x].ch[s1 ^ 1], fa, s1); cont(fa, x, s1 ^ 1); cont(x, gf, s2); update(fa), update(x); } void splay(int x, int goal) { while (T[x].fa != goal) { int fa = T[x].fa, gf = T[fa].fa; if (gf != goal) (wh(x) == wh(fa)) ? rotate(fa) : rotate(x); rotate(x); } if (! goal) root = x; } void insert(int v) { int x = root, fa = 0; while (x && T[x].val != v) fa = x, x = T[x].ch[T[x].val < v]; if (x) T[x].cnt ++; else { x = ++ cnt; if (fa) T[fa].ch[T[fa].val < v] = x; T[x].fa = fa, T[x].sz = 1, T[x].cnt = 1, T[x].val = v; } splay(x, 0); } int find(int v) { int x = root; while (T[x].val != v && T[x].ch[T[x].val < v]) x = T[x].ch[T[x].val < v]; return x; } //find该节点,旋转到根后输出左子树的权值就好 int getrank(int x) { splay(find(x),0); return T[T[root].ch[0]].sz; } //k == 0 找前驱,k == 1 找后继 //将该节点旋转到根后返回左子树最右边的节点,否则返回右子树最左边的节点 int getpre(int v, int k) { splay(find(v), 0); //将最大小于等于v的节点splay到根 int x = root; if (!k && T[x].val < v) return x; if (k && T[x].val > v) return x; x = T[x].ch[k]; while (T[x].ch[k ^ 1]) x = T[x].ch[k ^ 1]; return x; } int getnum(int rk) { int x = root; while (1) { pushdown(x); int ls = T[x].ch[0], rs = T[x].ch[1]; if (rk <= T[ls].sz) x = ls; else if (rk > T[ls].sz + T[x].cnt) rk -= T[ls].sz + T[x].cnt, x = rs; else return T[x].val; } } void del(int v) { int lt = getpre(v, 0); int ne = getpre(v, 1); splay (lt, 0), splay (ne, lt); if (T[T[ne].ch[0]].cnt > 1) T[T[ne].ch[0]].cnt --, splay(T[ne].ch[0],0); else T[ne].ch[0] = 0; } int main() { cin >> n; insert(- inf), insert(inf); for (int i = 1, p, x; i <= n; i ++) { scanf("%d %d", &p, &x); if (p == 1) insert (x); if (p == 2) del (x); if (p == 3) printf("%d ", getrank(x)); if (p == 4) printf("%d ", getnum(x + 1)); if (p == 5) printf("%d ", T[getpre(x, 0)].val); if (p == 6) printf("%d ", T[getpre(x, 1)].val); } return 0; }

    文艺平衡树代码相似,只要多维护一个序列翻转的tag,每次下传的时候记得翻转就好了

    //文艺平衡树
    #include<bits/stdc++.h>
    #define lrqtxdy cout << "lrqtxdy" << endl
    using namespace std;
    const int inf = 0x3f3f3f3f;
    struct st{
        int sz, cnt, ch[2], tag, fa, val;    
    }T[1000101];
    int root, cnt, n, m, l, r;
    void update (int x) { T[x].sz = T[T[x].ch[0]].sz + T[T[x].ch[1]].sz + T[x].cnt; }
    // 更新权值, 当前子树大小等于左边儿子大小加上右边儿子大小加上当前节点 
    int wh (int x) { return T[T[x].fa].ch[1] == x; }
    //查询当前节点是左儿子还是右儿子 
    void con(int x, int fa, int p) { T[fa].ch[p] = x,  T[x].fa = fa; }
    void rotate (int x) {
        int fa = T[x].fa, gf = T[fa].fa;
        //fa是当前节点的父亲,gf是父亲的父亲(grandfather) 
        bool s1 = wh(x), s2 = wh(fa);
        con(T[x].ch[s1 ^ 1], fa, s1);
        con(fa, x, s1 ^ 1);
        con(x, gf, s2);
        //旋转 
        update(fa), update(x);
        //更新权值 
    }
    void splay(int x, int pos) {
        for (int i; (i = T[x].fa) != pos; rotate(x))
            if (T[i].fa != pos)
                rotate((wh(x) == wh(i)) ? i : x);
        if (!pos) root = x;
    }
    void insert (int v) {
        //lrqtxdy;
        int x = root, fa = 0;
        while (x && T[x].val != v) fa = x, x = T[x].ch[T[x].val < v];
        //跑到这个节点的fa 
        if (x) T[x].cnt ++;
        //已经存在的话就把这个数的计数器++ 
        else {
             x = ++ cnt;
             if (fa) T[fa].ch[T[fa].val < v] = x;
             T[x].fa = fa, T[x].sz = 1, T[x].cnt = 1, T[x].val = v;
        }
        //插入新节点 
        splay (x, 0);
    }
    void pushdown(int x) {
        if(T[x].tag) {
            T[T[x].ch[0]].tag ^= 1, T[T[x].ch[1]].tag ^= 1, T[x].tag = 0;
            swap (T[x].ch[0], T[x].ch[1]);
        }
    }
    //如果当期节点有打标记,把左右儿子换一下 
    int kth(int rk) {
        int x = root;
        while (1) {
              pushdown(x);
              //pushdown后再计算,结果才是准确的 
              int ls = T[x].ch[0], rs = T[x].ch[1]; 
              if (rk <= T[ls].sz) x = ls;
              //如果排名比左边子树的大小还小的话,找左子树 
              else if (rk > T[ls].sz + T[x].cnt) rk -= T[ls].sz + T[x].cnt, x = rs;
              //如果排名比右边子树 + 自己本身数的个数大的话找右边儿子,找右子树 
            else return x;
        }
    }
    
    //打印树的中序遍历结果,输出就是答案 
    int print(int x) {
        pushdown(x);
        //记得pushdown 
        if (T[x].ch[0]) print(T[x].ch[0]);
        if (T[x].val != - inf && T[x].val != inf) printf("%d ", T[x].val);
        if (T[x].ch[1]) print(T[x].ch[1]);
    }
    int main() {
        cin >> n >> m;
        insert (- inf), insert (inf);
        for (int i = 1; i <= n; i ++) insert(i);
        while (m --) {
            scanf("%d %d", &l, &r);
             splay (kth(l), 0), splay (kth(r + 2), kth(l));
             T[T[T[root].ch[1]].ch[0]].tag ^= 1;
        }
        print(root);
        return 0;
    }
     
  • 相关阅读:
    元组转换列表
    python切片
    序列类型的方法 增删改查
    python基础 四则运算和数据类型
    linux 常用基础命令操作
    MySQL 命令操作
    linux中如何修改root密码、设置固定IP、安装vmware tools
    虚拟机中网络桥接模式设置
    PHP基础
    HTML基本标签介绍
  • 原文地址:https://www.cnblogs.com/liujunxi/p/14412451.html
Copyright © 2020-2023  润新知