FHQ-Treap(无旋Treap)

FHQ Treap

treap是一种基于随机索引值的二叉搜索树,因其在重构树的过程中也同时按随机索引值来维护该二叉搜索树具有二叉堆的性质

故得名Treap(Tree + Heap)

fhq treap则是一种不依赖于旋转操作的平衡树(所以好写

其结点一般保存左右儿子索引、当前结点值、堆索引、当前结点大小(包含子树)

struct node{
   int l, r;
   int key, val;
   int size;
};
const int maxn = 1e5 + 50;
node fhq[maxn];
int cnt, root; //计数器&根节点编号

提前开内存池,避免动态申请内存的额外开销。

inline int newnode(int val){
   int now = ++cnt;
   fhq[now].val = val;
   fhq[now].l = fhq[now].r = 0;
   fhq[now].size = 1;
   fhq[now].key = rnd();
   return now;
}

上面的key是堆索引,可以使用rand()取随机数

这里使用random库的mt19937随机数生成器生成随机数

#include<random>
std::mt19937 rnd(233);

 

其核心操作有二:

按值val分裂整棵树 一棵树的全部结点值 ≤ val 另一棵树的全部结点值 > val

inline void split(int now, int val, int &x, int &y){
   if(!now) x = y = 0;
   else{
       if(fhq[now].val <= val){
           x = now;
           split(fhq[now].r, val, fhq[now].r, y);
      }else{
           y = now;
           split(fhq[now].l, val, x, fhq[now].l);
      }
       update(now);
  }
}

思路是这样的

边界条件:当前已经递归到空结点了,那么x = y = 0(对空结点分裂大家肯定都空)

不然的话就比较当前结点和分裂值的大小

1.当前结点值≤val then 该结点以及该结点的左子树应该都划给结点x,然后递归地分裂当前结点的右子树(且结合二叉搜索树性质, 右子树中≤val的应当作为x结点的右子树)

2.同理,对于>当前结点的值的,则将该结点的右子树全部划分给结点y,然后递归地分裂该结点的左子树

在分裂完之后重新更新结点的size,因为会递归到所有size发生变化的结点,因此update只需要把size向上pushup即可。

inline void update(int now){
   fhq[now].size = fhq[fhq[now].l].size + fhq[fhq[now].r].size + 1;
}

 

第二个操作是合并(分裂完了得给人家拼起来啊……

inline int merge(int x, int y){
   if(!x || !y) return x+y;
   else{
       if(fhq[x].key > fhq[y].key){
           fhq[x].r = merge(fhq[x].r, y);
           update(x);
           return x;
      }else{
           fhq[y].l = merge(x, fhq[y].l);
           update(y);
           return y;
      }
  }
}

合并merge是基于随机索引key来维护二叉堆性质来对该二叉搜索树进行合并的

怎么理解经过merge几乎是随机合并得到的新二叉搜索树能够维持平衡树的平衡而使得二叉搜索树的常规操作维持在O(logN)级别呢?

可以这么理解: 如果一组数据是按大小顺序插入二叉搜索树的

比如1 2 3 4 5 那么该二叉搜索树将退化成链表

但如果我们插入时是经过随机化的,即变成比如3 5 2 4 1的顺序插入

那么将是一棵非常漂亮的平衡树。

如果将此处的

if(fhq[x].key > fhq[y].key)

换成

if(rnd() % 2)

treap的性能会被较大程度地影响(可能性能会更好…),但仍然能够在大多数情况下维持平衡。

FHQ Treap的插入、删除、求第k大、求x的排名均可通过split和merge操作来实现

……to be continued (写作业去)

 

方便起见:

我们声明三个全局变量用来存放以下操作所需要的树的根节点

int x, y, z;

 

插入操作insert

插入一个值为val的结点

可以用split和merge来实现

先将整棵树按值val分裂,然后将左子树和新结点merge,再将新的树和y merge

inline void insert(int val){
    split(root, val, x, y);
    root = merge(merge(x, newnode(val)), y);
}

 

删除操作del

删除一个值为val的结点

可以先将树按值val分裂成x和y

再将树x按值val-1分裂成x和z

这样分裂两次树z中仅有值为val的结点

我们只需要通过

z = merge(fhq[z].l, fhq[z].r);

即可将z树的根节点删除

所以del的代码就很容易写了-删除掉根节点之后再将三部分merge起来就OK了

inline void del(int val){
    split(root, val, x, y);
    split(x, val-1, x, z);
    z = merge(fhq[z].l, fhq[z].r);
    root = merge(merge(x, z), y);
}

 

求一个数的前驱

inline void pre(int val, int &res){
    split(root, val-1, x, y);
    int now = x;
    while(fhq[now].r) now = fhq[now].r;
    res = fhq[now].val;
    root = merge(x, y);
}

思路比较显然:将树按val-1分裂为x和y

再在较小的树上找到最大的值即为该数的前驱

求一个数的后缀

inline void nxt(int val, int &res){
    split(root, val, x, y);
    int now = y;
    while(fhq[now].l) now = fhq[now].l;
    res = fhq[now].val;
    root = merge(x, y);
}

思路和求pre类似,按值val分裂成x, y

然后在较大树上找最小值即为val的后缀

 

求排名为k的数

以第k小为例子,其他的排名规则思想一致

主要利用二叉搜索树的性质

1.如果当前结点+左子树的size 恰好等于k 则该结点的值即为第k小的数

2.如果当前结点+左子树size小于k,则第k大应在该结点的左子树上,在左子树上递归查找第k大

3.如果当前结点+左子树size大于k,则第k大应在结点的右子树上,在右子树上找第k - 左子树+根节点大小 大的数即可。

inline int getnum(int rank){
    int now = root;
    while(now){
        if(fhq[fhq[now].l].size + 1 == rank) break;
        else if(fhq[fhq[now].l].size + 1 > rank){
            now = fhq[now].l;
        }else now = fhq[fhq[now].r], rank -= (fhq[fhq[now].l].size + 1);
    }
    return fhq[now].val;
}

 

求数val的排名

排名定义依然采取越小排名越高的规则。

将树按值val-1分裂

在分裂得到的较小树的大小即为比val小的数字个数

再+1即为val的排名

inline void getrank(int val, int &res){
    split(root, val-1, x, y);
    res = fhq[x].size + 1;
    merge(x, y);
}

 

posted @ 2020-11-19 18:39  IrIrIrllleaf  阅读(336)  评论(0编辑  收藏  举报