2022年01月15日,第二天
平衡树—— F H Q T r e a p FHQ\ Treap FHQ Treap(无旋)
学习 f h q T r e a p fhq\ Treap fhq Treap 之前,要先了解普通的 T r e a p Treap Treap 是怎么回事。
T r e a p Treap Treap ,就是 T r e e Tree Tree 加 H e a p Heap Heap 。 它让平衡树上的每一个结点存放两个信息:值和一个随机的索引。其中值满足二叉搜索树的性质,索引满足堆的性质,结合二叉搜索树和二叉堆的性质来使树平衡。这也是 T r e a p Treap Treap 的性质。
T r e a p Treap Treap 为什么可以平衡?我们都知道,如果对一颗二叉搜索树进行插入的值按次序是有序的,那么二叉搜索树就会退化成一个链表。那么我们就可以别让数值按次序插入,一个很好的方法就是把插入的数值随机化,原因是大量数据时,随机化后变成有序的概率几乎为零。
T r e a p Treap Treap 用二叉堆来维护随机索引,其实就是相当于把插入次序随机化。插入一数值后你必然要让索引满足二叉堆的特性,但又因为索引是随机的,那就会导致插入的数不知道搞到了哪里去了,相当于插入次序随机了。
二叉搜索树的性质:当前结点左子树的值都比当前结点的值小,右子树的值都不比当前结点小,也就是大于等于。
二叉堆的性质:父结点的 优先级 总是大于或等于任何一个子节点的 优先级。
普通的 T r e a p Treap Treap 用来维护树平衡的核心操作是树旋转,而 f h q T r e a p fhq\ Treap fhq Treap 的核心操作有两个,分别是 分裂 ( s p l i t ) (split) (split) 和 合并 (merge) ,常数稍大。
结点, f h q T r e a p fhq\ Treap fhq Treap 的结点需要维护一下五个信息:
- 左右子树编号
- 元素的值
- 索引
- 子树大小
struct node {
int l, r;
int val, key;
int size;
}fhq[N];
int cnt, root;
#include <random>
mt19937 rnd(233);
inline int newnode(int val) {
fhq[++ cnt].val = val;
fhq[cnt].key = rnd();
fhq[cnt].size = 1;
return cnt;
}
核心操作——分裂 ( s p l i t ) (split) (split)
分裂有两种:按值分裂和按大小分裂。
按值分裂:把树拆成两颗树,拆出来的一棵树的值全部小于等于给定的值,另外一部分的值全部大于给定的值。
按大小分裂:把树拆成两颗树,其中一颗树的大小等于给定的大小,剩余部分在另一颗数里。
一般来说,我们在使用 f h q T r e a p fhq\ Treap fhq Treap 当一颗正常的平衡树用的时候,使用按值分裂,而在维护区间信息的时候,使用按大小分裂。很经典的例子就是 文艺平衡树。
inline void pushup(int now) {
fhq[now].size = fhq[fhq[now].l].size + fhq[fhq[now].r].size + 1;
}
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);
}
pushup(now);
}
}
核心操作——合并 ( m e r g e ) (merge) (merge)
本质上是分裂的逆向操作。
int merge(int x, int y) {
if (! x || ! y) return x | y;
if (fhq[x].key > fhq[y].key) {
fhq[x].r = merge(fhq[x].r, y);
pushup(x);
return x;
} else {
fhq[y].l = merge(x, fhq[y].l);
pushup(y);
return y;
}
}
第一个操作:插入
设插入的值为 v a l val val ,那么我们的步骤就是:按值 v a l val val 把树分裂成 x x x 和 y y y ,合并 x x x ,新结点, y y y 。
可以这么做的原因:我们要插入 v a l val val 这个值,那么我们按值 v a l val val 分裂,于是得到了两颗树 x x x 和 y y y ,按照按值分裂的定义,我们分裂出来的 x x x 树上的所有值一定都小于等于 v a l val val , y y y 树上的所有值一定都大于 v a l val val ,那么我们就可直接合并 x x x ,用值 v a l val val 新建的新结点, y y y 就可以了。
int x, y, z;
inline void insert (int val) {
split (root, val, x, y);
root = merge (merge (x, newnode(val)), y);
}
第二个操作:删除
设要删除的值为 v a l val val ,那么:首先,按值 v a l val val 把树分裂成 x x x 和 z z z ,再按值 v a l − 1 val - 1 val−1 把树分裂成 x x x 和 y y y ,那么此时 y y y 树上的所有值都是等于 v a l val val 的,我们去掉它的根结点:让 y y y 等于合并 y y y 的左子树和 y y y 的右子树。
inline void del (int val) {
split (root, val, x, z);
split (x, val - 1, x, y);
y = merge (fhq[y].l, fhq[y].r);
root = merge (merge (x, y), z);
}
第三个操作:查询值的排名
设要查询的值为 v a l val val ,那么:按值 v a l − 1 val-1 val−1 分裂成 x x x 和 y y y ,则 x x x 的大小 + 1 +1 +1 就是 v a l val val 的排名,最后将 x x x 和 y y y 合并起来。
inline int getrank (int val) {
split (root, val - 1, x, y);
int ans = fhq[x].size + 1;
root = merge (x, y);
return ans;
}
第四个操作:查询排名的值
和替罪羊树的做法一样。
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 >= rank)
now = fhq[now].l;
else {
rank -= fhq[fhq[now].l].size + 1;
now = fhq[now].r;
}
}
return fhq[now].val;
}
第五个操作:前驱和后继
设操作数为 v a l val val ,前驱:按值 v a l − 1 val-1 val−1 分裂成 x x x 和 y y y ,则 x x x 里面最右的数就是 v a l val val 的前驱。后继:按值 v a l val val 分裂成 x x x 和 y y y ,则 y y y 里面最左的数就是 v a l val val 的后继。
inline int pre (int val) {
split (root, val - 1, x, y);
int now = x;
while (fhq[now].r)
now = fhq[now].r;
int ans = fhq[now].val;
root = merge (x, y);
return ans;
}
inline int nxt (int val) {
split (root, val, x, y);
int now = y;
while (fhq[now].l)
now = fhq[now].l;
int ans = fhq[now].val;
root = merge (x, y);
return ans;
}