2022年01月15日,第二天
平衡树——FHQ TreapFHQ\ TreapFHQ Treap(无旋)
学习 fhq Treapfhq\ Treapfhq Treap 之前,要先了解普通的 TreapTreapTreap 是怎么回事。
TreapTreapTreap ,就是 TreeTreeTree 加 HeapHeapHeap 。 它让平衡树上的每一个结点存放两个信息:值和一个随机的索引。其中值满足二叉搜索树的性质,索引满足堆的性质,结合二叉搜索树和二叉堆的性质来使树平衡。这也是 TreapTreapTreap 的性质。
TreapTreapTreap 为什么可以平衡?我们都知道,如果对一颗二叉搜索树进行插入的值按次序是有序的,那么二叉搜索树就会退化成一个链表。那么我们就可以别让数值按次序插入,一个很好的方法就是把插入的数值随机化,原因是大量数据时,随机化后变成有序的概率几乎为零。
TreapTreapTreap 用二叉堆来维护随机索引,其实就是相当于把插入次序随机化。插入一数值后你必然要让索引满足二叉堆的特性,但又因为索引是随机的,那就会导致插入的数不知道搞到了哪里去了,相当于插入次序随机了。
二叉搜索树的性质:当前结点左子树的值都比当前结点的值小,右子树的值都不比当前结点小,也就是大于等于。
二叉堆的性质:父结点的 优先级 总是大于或等于任何一个子节点的 优先级。
普通的 TreapTreapTreap 用来维护树平衡的核心操作是树旋转,而 fhq Treapfhq\ Treapfhq Treap 的核心操作有两个,分别是 分裂 (split)(split)(split) 和 合并 (merge) ,常数稍大。
结点,fhq Treapfhq\ Treapfhq 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;
}
核心操作——分裂 (split)(split)(split)
分裂有两种:按值分裂和按大小分裂。
按值分裂:把树拆成两颗树,拆出来的一棵树的值全部小于等于给定的值,另外一部分的值全部大于给定的值。
按大小分裂:把树拆成两颗树,其中一颗树的大小等于给定的大小,剩余部分在另一颗数里。
一般来说,我们在使用 fhq Treapfhq\ Treapfhq 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);
}
}
核心操作——合并 (merge)(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;
}
}
第一个操作:插入
设插入的值为 valvalval ,那么我们的步骤就是:按值 valvalval 把树分裂成 xxx 和 yyy ,合并 xxx ,新结点,yyy 。
可以这么做的原因:我们要插入 valvalval 这个值,那么我们按值 valvalval 分裂,于是得到了两颗树 xxx 和 yyy ,按照按值分裂的定义,我们分裂出来的 xxx 树上的所有值一定都小于等于 valvalval ,yyy 树上的所有值一定都大于 valvalval ,那么我们就可直接合并 xxx ,用值 valvalval 新建的新结点,yyy 就可以了。
int x, y, z;
inline void insert (int val) {
split (root, val, x, y);
root = merge (merge (x, newnode(val)), y);
}
第二个操作:删除
设要删除的值为 valvalval ,那么:首先,按值 valvalval 把树分裂成 xxx 和 zzz ,再按值 val−1val - 1val−1 把树分裂成 xxx 和 yyy ,那么此时 yyy 树上的所有值都是等于 valvalval 的,我们去掉它的根结点:让 yyy 等于合并 yyy 的左子树和 yyy 的右子树。
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);
}
第三个操作:查询值的排名
设要查询的值为 valvalval ,那么:按值 val−1val-1val−1 分裂成 xxx 和 yyy ,则 xxx 的大小 +1+1+1 就是 valvalval 的排名,最后将 xxx 和 yyy 合并起来。
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;
}
第五个操作:前驱和后继
设操作数为 valvalval ,前驱:按值 val−1val-1val−1 分裂成 xxx 和 yyy ,则 xxx 里面最右的数就是 valvalval 的前驱。后继:按值 valvalval 分裂成 xxx 和 yyy ,则 yyy 里面最左的数就是 valvalval 的后继。
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;
}
本文介绍了FHQ Treap的基本概念及实现原理,包括其与普通Treap的区别、关键操作如分裂与合并的具体实现,以及如何利用这些操作完成插入、删除等操作。
893

被折叠的 条评论
为什么被折叠?



