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

 

 

蒟蒻的平衡树学习笔记

动态更新,在整理QWQ

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

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

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

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

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

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

核心思路

只要摸透了关键的旋转,splay,insert操作,这个数据结构其实很好理解的,

而这些操作的关键就是弄清楚对于一个节点,他的父亲和左右儿子这三个节点的关系

代码来自于大家广泛采用的模板,故较为简洁,但不可直接死记

一定要纸上模拟,摸清楚这些操作每一步是如何连接的,感受其构造精妙

这也是后续学习很多算法的核心——add by liujunxi 2024.3.28

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\n", getrank(x)); if (p == 4) printf("%d\n", getnum(x + 1)); if (p == 5) printf("%d\n", T[getpre(x, 0)].val); if (p == 6) printf("%d\n", 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;
}
 

 

posted @ 2021-02-18 16:44  liujunxi  阅读(79)  评论(0编辑  收藏  举报