SBT

  1. 参考:

    1. 算法训练营5.3

    2. 大佬板子:https://blog.csdn.net/l961983207/article/details/78626026

  2. 简介:

    1. 名称:SBT(Size Balanced Tree,节点大小平衡树)。

    2. 本质:是一种自平衡二叉查找树,通过旋转来保证子树的大小大致相等来保持平衡,更容易实现。在O(logn)时间内完成所有二叉搜索树的相关操作。

    3. 一些abstract:和普通的二叉搜索树相比,SBT仅加入了简洁的核心操作maintain。因为SBT用来保持平衡的是size域,所以当查询操作比较多的时候,因为节点的期望高度接近,SBT会有优势。另外,SBT保存的子树大小可以用来查询第k大,而AVL,Treap,红黑树存储的额外信息不能用来查询第k大。其中SBT的高度是O(logn)的,维护操作均摊下来是O(1)的,其余主要操作复杂度是O(logn)的。

  3. 操作:

    概括一下就是叔叔的子树大小一定大于侄子的子树大小,如图1所示:

     

    带虚线的部分要保证上子树size大于下子树size。

    板子:

     
    #include<algorithm>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    const int N=100100;int M;
    struct ss{
        int key[N], size[N], left[N], right[N], count[N];
        int rt, nodecnt;
        void clear(){
            rt=0,nodecnt=0;
            memset(key,0,sizeof(key));
            memset(s,0,sizeof(s));
            memset(left,0,sizeof(left));
            memset(right,0,sizeof(right));
              memset(count,0,sizeof(count));
        }
        void z_x(int &p){  // 左旋 
            int k=right[p];
            right[p]=left[k];left[k]=p;
            size[k]=size[p];
            size[p]=size[right[p]]+size[left[p]]+count[p];
            p=k; 
        }
          void y_x(int &p){  // 右旋
            int k=left[p];
            left[p]=right[k];right[k]=p;
            size[k]=size[p];
            size[p]=size[right[p]]+size[left[p]]+count[p];
            p=k;
        }
        void peace(int &p, bool flag){
              if (!p) return;
            if(!flag) {  // 加到了左子树 
                if(size[left[left[p]]] > size[right[p]]) y_x(p);
                else {  // 加到了左子树的右子树 
                    if (size[right[left[p]]]>size[right[p]]) {
                        z_x(left[p]);y_x(p);
                    }
                      else return;
                }
            } else {  // 加到右子树
                if (size[right[right[p]]] > size[left[p]]) z_x(p);
                else {
                    if (size[left[right[p]]]>size[left[p]]){
                        y_x(right[p]);z_x(p);
                    }
                      else return;
                }
            }
            peace(left[p],false);
            peace(right[p],true);
            peace(p,true);
            peace(p,false);
        }
        void insert(int &p,int x){  // 在根p的子树中,加入一个值为x的数 
          if(!p){  // 没出现过
            p=++nodecnt;
            if (nodecnt == 1) rt = 1;
            key[p]=x;
            size[p]=count[p]=1;
            return;
          }
          size[p]++;
          if (x < key[p]) insert(left[p],x);
          else if (x > key[p]) insert(right[p],x);
          else {
            count[p]++;
            return;
          }
          peace(p,x > key[p]);
        }
            
          void remove(int &p, int val, int type) {  // 删除一个值为val的数 type为0是正常减少一个,type为1是全部删除
          if (!p) return;
          size[p]--;
          if (key[p] == val) {
            if (count[p] > 1 && type == 0) {  // 必须删一个且大于一个的情况才可以直接返回
              count[p]--;
              return;
            }
            if (!left[p] || !right[p]) {  // 有一个为空,则直接用不为空的儿子代替这个节点
              p = left[p] + right[p];
            } else {  // 先令直接后继代替此点,然后删除直接后继
              int tmp = right[p];
              while(left[tmp]) {
                tmp = left[tmp];
              }
              key[p] = key[tmp];
              count[p] = count[tmp];
              remove(right[p], key[tmp], 1);  // 必须全部删掉
            }
          }
          else if (val < key[p]) remove(left[p], val, type);  // 继承删除的方法
          else remove(right[p], val, type);
        }
      
          int find_v(int &p, int val) {  // 根据val在p子树中查找
          if (!p || key[p] == val) return p;  // 确定没找到或者找到
          if (val < key[p]) return find_v(left[p], val);
          else return find_v(right[p], val);
        }
      
          int getMin() {
          if (!rt) return -inf;
          int p = rt;
          while(left[p]) p = left[p];
          return key[p];
        }    
      
          int getMax() {
          if (!rt) return inf;
          int p = rt;
          while(right[p]) p = right[p];
          return key[p];
        }
      
        int rank(int &p, int val) {  // val的排名 
            if(!p) return 1;
              int tmp=0;
            if(val <= key[p]) tmp = rank(left[p], val);
            else tmp = size[left[p]] + count[p] + rank(right[p], val);
            return tmp;
        }
        int select(int &p, int x){  // 第x小的数 
          if(x >= size[left[p]] + 1 && x <= size[left[p]] + count[p]) return key[p];
          if(x <= size[left[p]]) return select(left[p], x);
          else return select(right[p], x - count[p] - size[left[p]]);
        }
        int getPre(int &p, int q, int val) {  // 求val在子树p中的前驱 q记录了之前找到的答案位置
          if (!p) return key[q];
          if (key[p] < val) {
            return getPre(right[p], p, val);  // p的值不够,尝试往大了找,说明q可以更好,更新为p。然后q就放小于val的p的位置,防止找过头
          }
          return getPre(left[p], q, val);  // p的值够了,得往小了找,q保持原状,不能超过val
        }
          int getNxt(int &p, int q, int val) {  // 求val在子树p中的后继 q记录了之前找到的答案位置
          if (!p) return key[q];
          if (key[p] > val) {
            return getNxt(left[p], p, val);  // p的值够了,尝试往小了找,说明q可以更好,更新为p。然后q就放大于val的p的位置,防止找过头
          }
          return getNxt(right[p], q, val);  // p的值不够,得往大了找,q保持原状,不能小于等于val
        }
    }T;
    
    int main(){
        T.clear();scanf("%d",&M);
        int &rt=T.rt=0;
        while(M--){
            int opt,x;scanf("%d%d",&opt,&x);
            if(opt==1) T.insert(rt,x);
            else if(opt==2) T.remove(rt, x);
            else if(opt==3) printf("%d\n",T.rank(rt,x));
            else if(opt==4) printf("%d\n",T.select(rt,x));
            else if(opt==5) printf("%d\n",T.getPre(rt, rt, x));
            else if(opt==6) printf("%d\n",T.getNxt(rt, rt, x)); 
        }
          return 0;
    }

     

    find_v(int &p, int val) {  // 根据val在p子树中查找 
          if (!|| key[p] == val) return p;  // 确定没找到或者找到 
          if (val key[p]) return find_v(left[p], val); 
          else return find_v(right[p], val); 
       } 
       
      int getMin() { 
          if (!rt) return -inf; 
          int rt; 
          while(left[p]) left[p]; 
          return key[p]; 
       } 
       
      int getMax() { 
          if (!rt) return inf; 
          int rt; 
          while(right[p]) right[p]; 
          return key[p]; 
       } 
       
        int rank(int &p, int val) {  // val的排名  
            if(!p) return 1; 
          int tmp=0; 
            if(val <= key[p]) tmp rank(left[p], val); 
            else tmp size[left[p]] count[p] rank(right[p], val); 
            return tmp; 
       } 
        int select(int &p, int x){  // 第x小的数  
            if(x==size[left[p]]+1) return key[p]; 
            if(x<=size[left[p]]) return select(left[p],x); 
            else return select(right[p],x-1-size[left[p]]); 
       } 
        int getPre(int &p, int q, int val) {  // 求val在子树p中的前驱 q记录了之前找到的答案位置 
          if (!p) return key[q]; 
          if (key[p] val) { 
            return getPre(right[p], p, val);  // p的值不够,尝试往大了找,说明q可以更好,更新为p。然后q就放小于val的p的位置,防止找过头 
         } 
          return getPre(left[p], q, val);  // p的值够了,得往小了找,q保持原状,不能超过val 
       } 
      int getNxt(int &p, int q, int val) {  // 求val在子树p中的后继 q记录了之前找到的答案位置 
          if (!p) return key[q]; 
          if (key[p] val) { 
            return getNxt(left[p], p, val);  // p的值够了,尝试往小了找,说明q可以更好,更新为p。然后q就放大于val的p的位置,防止找过头 
         } 
          return getNxt(right[p], q, val);  // p的值不够,得往大了找,q保持原状,不能小于等于val 
       } 
    }T; 
    ​ 
    int main(){ 
        T.clear();scanf("%d",&M); 
        int &rt=T.rt=0; 
        while(M--){ 
            int opt,x;scanf("%d%d",&opt,&x); 
            if(opt==1) T.insert(rt,x); 
            else if(opt==2) T.remove(rt, x); 
            else if(opt==3) printf("%d\n",T.rank(rt,x)); 
            else if(opt==4) printf("%d\n",T.select(rt,x)); 
            else if(opt==5) printf("%d\n",T.getPre(rt, rt, x)); 
            else if(opt==6) printf("%d\n",T.getNxt(rt, rt, x));  
       } 
      return 0; 
    }
    1. 旋转:左旋和右旋。旋转也是maintain操作的基础。

       

      连图都和Treap一模一样,旋转的方式也一样(不懂可以去Treap篇看一下),直接上板子:

      右旋:

       

      左旋:

       

    2. 维护:

      当插入节点之后,有可能会出现不满足SBT的性质,要根据插入位置的不同,进行相应的旋转,以达到平衡状态,于是就会出现四种情况:LL、LR、RL、RR。

      1. LL型。即图1中的size[R] < size[A],而size[B] <= size[R](因为插入的是LL所以右侧之前是平衡的,一定满足这个条件),如图2所示:

         

        这时就要右旋T,让A往上走一走。

        这时size[B] < size[A],size[R] < size[A],对于T来说,因为size[B] <= size[R],所以不会出现LL和LR的情况,只能出现RL或RR的情况(只能出现一种,不然R是B的二倍多,但是没有A大,说明A是B的两倍多,则A必有一个儿子大于B,那原来就已经不平衡了,矛盾),所以递归查看即可,注意T调整之后,再回来调整L,因为右侧可能已经不是T做父亲了。

      2. LR型。即图1中的size[R] < size[B],而size[A] <= size[R],但是情况比想象中的更要复杂,如图3所示:

         

        为什么不像刚才那样T右旋呢?虽然可以让T的子树都满足平衡条件,但是size[A] < size[B]且size[A] < size[R]。所以相当于有了更大的麻烦,看来不是简单的T右旋。

        尝试先让L左旋,这样让B上来和R同级,但是注意因为原来size[A] >= size[E] && size[A] >= size[F]。L左旋后,问题变成了子树B中A和F的矛盾,好像还不如原来,所以第二步T右旋,对于B的左子树而言,因为原来的L是平衡的,所以size[A] >= size[E],所以左子树L旋转后不存在RR和RL的情况,判断LL和LR的情况即可;而对于B的右子树而言,因为size[R] >= size[A] >=size[F],所以B的右子树T中不存在LL和LR的情况,只需要判断RL和RR的情况。左右子树全部调整完之后,最后再看B这个树的整体情况即可。

      3. RL和LR对称,不用说了。

      4. RR和LL对称,不用说了。

      总结一下:

      不管怎么样,在调整完儿子之后,最后都要“彻查”根节点,所以最后都要:

       peace(p,true);
       peace(p,false);

      在此之前,LL要看右子树的RL和RR;RR要看左子树的LL和LR;LR要看右子树的RL和RR以及左子树的LR和LL;RL要看左子树的LL和LR以及右子树的RL和RR,发现和LR是一样的。然后LR和RL也包括了LL和RR的操作,所以不论怎么样,都是调整左子树的左侧,右子树的右侧,然后是根节点左右再看一下,所以最后是:

       peace(left[p],false);
       peace(right[p],true);
       peace(p,true);
       peace(p,false);

      对应板子:

       void peace(int &p, bool flag){
         if(!flag) {  // 可能的LL或LR型
           if(size[left[left[p]]] size[right[p]]) y_x(p);  // 是LL型,右旋根
           else {  // 可能会有LR型
             if (size[right[left[p]]]>size[right[p]]) {  // 是LR型
               z_x(left[p]);y_x(p);  // 先左旋左儿子L,然后把旋后的根节点(因为是指针所以p已经变了)T右旋
            }
             else return;  // 不是LR型
          }
        } else {  // 可能的RL或RR型
           if (size[right[right[p]]] size[left[p]]) z_x(p);  // 是RR型,左旋根
           else {
             if (size[left[right[p]]]>size[left[p]]){  // 是RL型
               y_x(right[p]);z_x(p);  // 先右旋右儿子R,然后把旋后的根节点T左旋
            }
             else return;  // 不是RL型
          }
        }
         peace(left[p],false);
         peace(right[p],true);
         peace(p,true);
         peace(p,false);
       }
    3. 插入:

      板子:

       void insert(int &p,int x){  // 在根p的子树中,加入一个值为x的数 
         if(!p){  // 没出现过
           p=++nodecnt;
           key[p]=x;
           size[p]=count[p]=1;
           return;
        }
         size[p]++;
         if (key[p]) insert(left[p],x);
         else if (key[p]) insert(right[p],x);
         else {
           count[p]++;
       return;
        }
         peace(p,key[p]);
       }
    4. 删除:删除节点之后虽然不能保证这棵树是SBT,但是整棵树的最大深度没有变化,所以时间复杂度不会增加,所以此时维护操作显得多余,所以删除操作中没有调整平衡。

      板子:

       void remove(int &p, int val, int type) {  // 删除一个值为val的数 type为0是正常减少一个,type为1是全部删除
         size[p]--;
         if (key[p] == val) {
           if (count[p] && type == 0) {  // 必须删一个且大于一个的情况才可以直接返回
             count[p]--;
             return;
          }
           if (!left[p] || !right[p]) {  // 有一个为空,则直接用不为空的儿子代替这个节点
             left[p] right[p];
          } else {  // 先令直接后继代替此点,然后删除直接后继
             int tmp right[p];
             while(left[tmp]) {
               tmp left[tmp];
            }
             key[p] key[tmp];
             count[p] count[tmp];
             remove(right[p], key[tmp], 1);  // 必须全部删掉
          }
        }
         else if (val key[p]) remove(left[p], val, type);  // 继承删除的方法
         else remove(right[p], val, type);
       }
    5. 查找:

      板子:

       int find_v(int &p, int val) {
         if (!|| key[p] == val) return p;  // 确定没找到或者找到
         if (val key[p]) return find_v(left[p], val);
         else return find_v(right[p], val);
       }
    6. 最小&最大值:

      1. 最小值:

         int getMin() {
           if (!rt) return -inf;
         int rt;
           while(left[p]) left[p];
           return key[p];
         }
      2. 最大值:

        int getMax() {
        if (!rt) return inf;
        int p = rt;
        while(right[p]) p = right[p];
        return key[p];
        }
    7. 前驱&后继:

      1. 前驱:严格小于的,如果比所有数都小,返回树中最小的值

        int getPre(int &p, int q, int val) {  // 求val在子树p中的前驱 q记录了之前找到的答案位置
        if (!p) return key[q];
        if (key[p] < val) {
        return getPre(right[p], p, val); // p的值不够,尝试往大了找,说明q可以更好,更新为p。然后q就放小于val的p的位置,防止找过头
        }
        return getPre(left[p], q, val); // p的值够了,得往小了找,q保持原状,不能超过val
        }
      2. 后继:严格大于的,如果比所有数都大,返回树中最大的值

        int getNxt(int &p, int q, int val) {  // 求val在子树p中的后继 q记录了之前找到的答案位置
        if (!p) return key[q];
        if (key[p] > val) {
        return getNxt(left[p], p, val); // p的值够了,尝试往小了找,说明q可以更好,更新为p。然后q就放大于val的p的位置,防止找过头
        }
        return getNxt(right[p], q, val); // p的值不够,得往大了找,q保持原状,不能小于等于val
        }
    8. 排名:

      int rank(int &p, int val) {  // val的排名 
      if(!p) return 1;
      int tmp=0;
      if(val <= key[p]) tmp = rank(left[p], val); // 小于显然是该往左看,等于的话,找遍左边之后,会找到0,那个的1正好可以算中间的一个数
      else tmp = size[left[p]] + count[p] + rank(right[p], val); // 大于就是左边+中间+右边查找排名
      return tmp;
      }
    9. 第k小:

      int select(int &p, int x){  // 第x小的数 
      if(x >= size[left[p]] + 1 && x <= size[left[p]] + count[p]) return key[p];
      if(x <= size[left[p]]) return select(left[p], x);
      else return select(right[p], x - count[p] - size[left[p]]);
      }
  4. 例题:

    1. 第k小的数(HDU4217):有n个数字1~n,第i轮游戏中,每次拿出第ki小的数,并把它拿走,求出m轮游戏后拿走的数字总和。

      输入:T组数据(128),每组给出m、n(262144),然后给m个数,mi表示拿走的排名。

      输出:和是多少。

      最开始插入1到n,插入之后每次就查找,删除。

      ans = 0;
      while(m--) {
      scanf("%d", &k);
      int tmp = T.select(T.rt, k)
      ans += tmp;
      T.remove(T.rt, tmp, 1);
      }
    2. (好题!)区间第k小(POJ2761)。给出n和m,n表示数的数量,m表示查询的数量,对于每个查询,给出左边界和右边界,再给出k,要求给出给定区间的第k小的值。保证区间不会出现完全包含的情况。

      这题允许离线哎!!!并且最后一句话已经在明示按照端点排序了。

      于是将所有的询问按照左边界排序,左边界一样的按照右边界排序。然后对于查询q[i],先删除[q[i-1].l, q[i].l),然后插入后一部分的数据(q[i-1].r, q[i].r]。保证每次SBT都是存的当前查询区间的值,然后每次查询SBT中第k小的数输出。

      struct question {
      int l, r, k;
      int id;
      bool operator < (const question &b) const {
      return l < b.l;
      }
      }q[maxm];

      int main() {
      int n, m;
      scanf("%d %d", &n, &m);
      for (int i = 1; i <= n; i++) {
      scanf("%d", &a[i]);
      }
      for (int i = 1; i <= m; i++) {
      scanf("%d %d %d", &q[i].l, &q[i].r, &q[i].k);
      q[i].id = i;
      }
      sort(q+1, q+m+1);
      q[0].l = q[0].r = 0;
      for (int i = 1; i <= m; i++) {
      for (int j = q[i-1].l; j <= q[i-1].r && j < q[i].l; j++) {
      if (j == 0) continue;
      T.remove(T.rt, a[j], 1);
      }
      int r = (q[i-1].r >= q[i].l) ? (q[i-1].r + 1) : q[i].l;
      for (int j = r; j <= q[i].r; j++) {
      T.insert(T.rt, a[j]);
      }
      ans[q[i].id] = T.select(T.rt, q[i].k);
      }
      for (int i = 1; i <= m; i++) {
      printf("%d\n",ans[i]);
      }
      return 0;
      }
    3. 郁闷的出纳员(P1486)。

      输入:第一行包含两个数:命令数量n和员工的工资下限min。

      接下来输入n个命令:

      1. "I k"表示插入一个工资为k的员工,如果低于min,则立马将其辞职,好像不算在答案内。

      2. "A k"表示将每个员工的工资都加上k。

      3. "S k"表示将每个员工的工资都扣除k,不到min的辞职。

      4. "F k"表示查询工资第k多的工资。

      5. 开始时没有员工。

      数据范围:n(10^5),A和S命令不超过100,F命令不超过10^5个,每次调整量不超过1000,新员工工资不超过10^5。

      输出:对于每个F命令输出,若k大于员工数量,输出-1。最后一行输出辞职员工总数。

      算法设计:

      1. 设置变量add,记录增加的工资量,A命令直接add+=k即可。

      2. 插入工资k时,若k大于等于min,直接将k-add插入,其实就是一个相对值。

      3. 扣除k时,add-=k,然后开始一直循环,如果SBT不为空,则读出最小工资+add,如果小于min,删除该节点,ans++,知道所有员工的工资都不小于min为止。

      4. 查询时,若k大于size[rt],输出-1,不然查询第size[rt]-k+1小,输出即可。

       int main() {
         scanf("%d %d", &n, &Min);
         ans add 0;
         for (int 0; n; i++) {
           // ...省略读取过程
           if (ch == 'I' && >= Min) {
             T.insert(T.rt, add);
          }
           else if (ch == 'A') add += k;
           else if (ch == 'S') {
             add -= k;
             int a;
             while(T.size[T.rt] && (a=T.select(T.rt, 1)) add Min) {
               T.remove(T.rt, a, 1);
               ans+=T.size[a];
            }
          }
           else if (ch == 'F') {
             if (T.size[T.rt]) puts("-1");
             else printf("%d\n", T.select(T.rt, T.size[T.rt] 1) add);
          }
        }
         printf("%d\n", ans);
         return 0;
       }

       

posted on 2022-05-10 23:32  小染子  阅读(338)  评论(0)    收藏  举报