欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[总结] 平衡树总结

程序员文章站 2022-05-19 19:50:28
...

1. treap

  • 堆的性质
    treap = tree + heap
    也就是 treap 是具有堆性质的平衡二叉树(BST), 而堆性质的维护就靠一个随机值和旋转操作. 可以是小根堆也可以是大根堆.
    用 r 数组(random)保存随机值, 那么在插入结点时需要维护堆性质(这里是大根堆)
    ch[o][0] 、ch[o][1] 分别表示 结点o 的左儿子和右儿子.
    自己定义宏 :
    #define lc ch[o][0]
    #define rc ch[o][1]
    if(r[ch[o][d]] > r[o]) rotate(o, d^1);  // rotate见下方
    

  • 左旋和右旋 有太多的相似处, 可以用一个带旋转方向参数的 rotate 操作来完成
    d = 0 表示左旋, d = 1 表示右旋.
    void rotate(int& o, int d) { 
        int k = ch[o][d^1]; ch[o][d^1] = ch[k][d]; 
        ch[k][d] = o; update(o); update(k); o = k;
    }
    

  • 删除

    • 首先得找到要删除的结点
    • 如果该结点只有一个左儿子或只有一个右儿子, 直接将它把要删除的结点取代.

        if(lc * rc == 0) o = lc + rc; 
    • 否则就考虑堆性质的维护

       int d2 = tr[t.ch[0]].r < tr[t.ch[1]].r ? 1 : 0;
       rotate(o, d2); remove(o, x);
      

  • 查询
    • 查询第 k 大
      如果要查询的结点在当前结点的右儿子上, 那么继续查询右儿子的第k-s[lc]-1大.
    • 查询某结点排名
      如果要查询的结点在当前结点的右儿子上, 那么排名应该增加s[lc]+1.


2. splay

  • 和 treap 比较起来, splay 多的就是伸展(splay)操作.

  • 关于伸展操作

    • 两种实现方式 : 自顶向下和自底向上.
      自底向上的 splay 我是参考的 HZWER, 需要使用fa数组记录当前结点的父结点. 比较方便的是可以直接通过这个fa数组发现从一个结点走到任意已知结点的路径. 于是就诞生了自底向上的splay. 正因为这种特性, 这种伸展方式可以适应任何题目.

      void rotate(int& px, int& x, int d) {
          int t = ch[x][d]; ch[x][d] = px; ch[px][d^1] = t; 
          p[x] = p[px]; p[px] = x; p[t] = px; update(px); update(x); px = x;
      } 
      
       void splay(int x, int& k) {
          while(x != k) {
              int y = p[x], z = p[y];
              int d = ch[y][0] == x ? 0 : 1;
              int d2 = ch[z][0] == y ? 0 : 1;
              if(y != k) rotate(ch[z][d2], x, d^1); else rotate(k, x, d^1);
          }
      }
    • 自顶向下的 splay 可以看《训练指南》, 刘汝佳的代码. 比较简洁高效. 我是把指针改成了数组, 不习惯指针的写法, 主要是不知道怎么调试.

      void splay(int& o, int k) {
          int d = cmp(o, k);
          if(d == -1) return;
          if(d == 1) k -= s[lc] + 1;
          int p = ch[o][d];
          int d2 = cmp(p, k);
          int k2 = (d2 == 0) ? k : k-s[ch[p][0]]-1;
          if(d2 != -1) {
              splay(ch[p][d2], k2);
              if(d == d2) rotate(o, d^1); else rotate(ch[o][d], d);
          }
          rotate(o, d^1);
      } 
    • 自顶向下就需要知道你想 splay 的结点的排名. 那么有的题目比如 书架 那个题, 直接告诉你哪个结点编号, 但它是第几个结点不知道, 这样就只能用第一种自底向上的splay了.

    • 另外splay可以一次旋转两回也可以旋转一回, 即单旋和双旋. 单旋的方法比较简单, 整个过程相当于先找到第 k 个结点, 再递归自底向上旋转到目标结点. 不过在有些情况会退化成一条链O(n). 转两回的参考《训练指南》.
      void splay(int& o, int k) {
          int d = cmp(o, k);
          if(d == -1) return;
          if(d == 1) k -= s[lc] + 1;
          splay(ch[o][d], k); 
          rotate(o, d^1);
      } 

  • 其他操作和 treap 差不多, 但 splay 支持很多区间操作

    • 得到一段区间, 把它移动到一个固定结点.
      都采用自顶向下的splay
      注意这里在首部和末尾都添加一个虚拟结点防止溢出.
    // 移动到根结点右儿子的左儿子
    void getInterval(int o, int L, int R) {
        splay(o, L);
        splay(rc, R - s[lc] - 1);
    }
    // 移动到根结点左儿子的右儿子
    void getInterval(int o, int L, int R) {
        splay(o, L);
        splay(o, R + 2);
    }
    • 第二种方法只适用于单旋splay
    • 有了这个获取区间的题目, 就可以完成很多区间的题目, 比如区间修改、区间翻转、区间标记之类的.