|
|
##[$AcWing$ $253$. 普通平衡树](https://www.acwing.com/problem/content/description/255/)
|
|
|
|
|
|
### 一、题目描述
|
|
|
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
|
|
|
|
|
|
* 1 插入数值 $x$。
|
|
|
* 2 删除数值 $x$(若有多个相同的数,应只删除一个)。
|
|
|
* 3 查询数值 $x$ 的排名(若有多个相同的数,应输出最小的排名)。
|
|
|
* 4 查询排名为 $x$ 的数值。
|
|
|
* 5 求数值 $x$ 的前驱(前驱定义为小于 $x$ 的最大的数)。
|
|
|
* 6 求数值 $x$ 的后继(后继定义为大于 $x$ 的最小的数)。
|
|
|
|
|
|
注意: 数据保证查询的结果一定存在。
|
|
|
|
|
|
**输入格式**
|
|
|
第一行为 $n$,表示操作的个数。
|
|
|
|
|
|
接下来 $n$ 行每行有两个数 $opt$ 和 $x$,$opt$ 表示操作的序号($1≤opt≤6$)。
|
|
|
|
|
|
**输出格式**
|
|
|
对于操作 $3,4,5,6$ 每行输出一个数,表示对应答案。
|
|
|
|
|
|
**数据范围**
|
|
|
$1≤n≤100000$,所有数均在 $−10^7$ 到 $10^7$ 内。
|
|
|
|
|
|
**输入样例**:
|
|
|
```cpp {.line-numbers}
|
|
|
8
|
|
|
1 10
|
|
|
1 20
|
|
|
1 30
|
|
|
3 20
|
|
|
4 2
|
|
|
2 10
|
|
|
5 25
|
|
|
6 -1
|
|
|
```
|
|
|
|
|
|
**输出样例:**
|
|
|
```cpp {.line-numbers}
|
|
|
2
|
|
|
20
|
|
|
20
|
|
|
20
|
|
|
```
|
|
|
|
|
|
### 二、$STL+vector$
|
|
|
|
|
|
和我一起大声读:$STL$大法好!$645$ $ms$,比$Treap$平衡树慢一倍左右。
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
#include <cstdio>
|
|
|
#include <vector>
|
|
|
#include <algorithm>
|
|
|
#include <iostream>
|
|
|
using namespace std;
|
|
|
|
|
|
int n;
|
|
|
vector<int> a;
|
|
|
|
|
|
int main() {
|
|
|
//加快读入
|
|
|
ios::sync_with_stdio(false), cin.tie(0);
|
|
|
cin >> n;
|
|
|
int op, x;
|
|
|
while (n--) {
|
|
|
cin >> op >> x;
|
|
|
if (op == 1)
|
|
|
a.insert(lower_bound(a.begin(), a.end(), x), x); //插入数值x
|
|
|
else if (op == 2)
|
|
|
a.erase(lower_bound(a.begin(), a.end(), x)); //删除数值x
|
|
|
else if (op == 3)
|
|
|
printf("%d\n", lower_bound(a.begin(), a.end(), x) - a.begin() + 1); //查询数值 x 的排名
|
|
|
else if (op == 4)
|
|
|
printf("%d\n", a[x - 1]); //查询排名为 x 的数值
|
|
|
else if (op == 5)
|
|
|
printf("%d\n", *--lower_bound(a.begin(), a.end(), x)); //求数值 x 的前驱
|
|
|
else if (op == 6)
|
|
|
printf("%d\n", *upper_bound(a.begin(), a.end(), x)); //求数值 x 的后继
|
|
|
}
|
|
|
return 0;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 三、普通平衡树$Treap$
|
|
|
本题是一道普通平衡树$Treap$的引入模板题,是想让我们了解并开始学习平衡树的,用$STL$水过,一来可以让大家更好的学习$STL$,二来是没有背下来普通平衡树、$FHQ$树、$Splay$之前,有一技傍身,废话少说,开始操练:
|
|
|
|
|
|
$Treap$ 指 $Tree + heap$,又叫作 **树堆**,同时满足二叉搜索树和堆两种性质。二叉搜索树满足中序有序性,输入序列不同,创建的二叉搜索树也不同,在 **最坏的情况** 下(只有左子树或只有右子树)会退化为 **线性**。例如输入`1 2 3 4 5`,创建的二叉搜索树如下图所示。
|
|
|
<center><img src='https://img-blog.csdnimg.cn/4d38c4769837473d871112adcf78fcde.png'></center>
|
|
|
|
|
|
二叉搜索树 $BST$ 的插入、查找、删除等效率与树高成正比,因此在创建二叉搜索树时 **要尽可能通过调平衡压缩树高**。平衡树有很多种,例如 $AVL$ 树、伸展树、$SBT$、红黑树等,这些调平衡的方法相对复杂。
|
|
|
|
|
|
若一个二叉搜索树插入节点的顺序是随机的,则得到的二叉搜索树在大多数情况下是平衡的,即使存在一些极端情况,这种情况发生的概率也很小,因此 **以随机顺序创建的二叉搜索树**,其期望高度为$logn$。这是有数学定理证明过的,水平所限,不再详细论述。所以,可以将输入数据随机打乱,再创建二叉搜索树,但我们有时并不能事前得知所有待插入的节点,而$Treap$可以有效解决该问题。
|
|
|
|
|
|
在 $Treap$ 的构建过程中,插入节点时会给每个节点都 **附加一个随机数作为优先级**,**该优先级满足堆的性质**(最大堆或最小堆均可,这里以 **最大堆为例**,根的优先级大于左右子节点),数值满足二叉搜索树性质(中序有序性,左子树小于根,右子树大于根)。
|
|
|
|
|
|
#### $BST$的中序遍历 投影法则
|
|
|
<center><img src='https://img2022.cnblogs.com/blog/8562/202205/8562-20220507095633176-266944915.png'></center>
|
|
|
|
|
|
同样的中序遍历输出其实在真正的存储时,是不一样的形态。我们只要维持住中序遍历的次序不变,结合大顶堆的随机值在上的特点,就可以得到一个 **矮粗胖** 的平衡树,获取最好的性能。
|
|
|
|
|
|
#### 构建过程
|
|
|
|
|
|
输入 `6 4 9 7 2`,构建 $Treap$。首先给每个节点都附加一个随机数作为优先级,根据 **输入数据** 和 **附加随机数**,构建的 $Treap$ 如下图所示。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/76592ac878fa46c18971eb9711481529.png'></center>
|
|
|
|
|
|
#### 右旋和左旋
|
|
|
$Treap$ 需要两种神操作,通过两个神操作,才能保证平衡树不是一条链,而是又矮又胖,这两个神操作是:**右旋和左旋**
|
|
|
|
|
|
**右旋($zig$)**
|
|
|
节点 $p$ 右旋时,会携带自己的右子树,向右旋转到 $q$ 的右子树位置,$q$ 的右子树被抛弃,此时 $p$ 右旋后左子树正好空闲,将 $q$ 的右子树放在 $p$ 的左子树位置,旋转后的树根为 $q$ 。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/8978192505434831bf61c9eb7f136477.png'></center>
|
|
|
|
|
|
动画演示:
|
|
|
<center><img src='https://cdn.acwing.com/media/article/image/2021/10/29/52559_e3864c7b38-5057293-45da57f7ccfefe4d.gif'></center>
|
|
|
|
|
|
|
|
|
**左旋($zag$)**
|
|
|
节点 $p$ 左旋时,携带自己的左子树,向左旋转到 $q$ 的左子树位置,$q$ 的左子树被抛弃,此时 $p$ 左旋后右子树正好空闲,将 $q$ 的左子树放在 $p$ 的右子树位置,旋转后的树根为 $q$ 。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/d2dead3ba0fa4682a97b0451a057a6c3.png'></center>
|
|
|
|
|
|
动画演示:
|
|
|
<center><img src='https://cdn.acwing.com/media/article/image/2021/10/30/52559_c53b02bc39-5057293-3e5e8475936a5f0a.gif'></center>
|
|
|
|
|
|
|
|
|
#### 插入 $insert$
|
|
|
$Treap$ 的插入操作和二叉搜索树一样,首先根据有序性找到插入的位置,然后创建新节点插入该位置。创建新节点时,会给该节点附加一个随机数作为优先级,**自底向上检查** 该优先级是否满足堆性质,**若不满足,则需要右旋或左旋,使其满足堆性质。**
|
|
|
|
|
|
**算法步骤**:
|
|
|
|
|
|
* ① 从根节点 $p$ 开始,若 $p$ 为空,则创建新节点,将待插入元素 $key$ 存入新节点,并给新节点附加一个随机数$val$作为优先级
|
|
|
|
|
|
* ② 若 $key$ 等于 $tr[p].key$,则 $tr[p].cnt++$
|
|
|
|
|
|
* ③ 若 $key$ 小于 $tr[p].key$,则在 $p$ 的左子树中递归插入。回溯时做旋转调整,若 $tr[p].val < tr[p[l]].val $,则 $p$ **右旋**
|
|
|
|
|
|
* ④ 若 $key$ 大于 $tr[p].key$,则在 $p$ 的右子树中递归插入。回溯时做旋转调整,若 $tr[p].val < tr[p.r].val$ ,则 $p$ **左旋**
|
|
|
|
|
|
一个树堆如下图所示,在该树堆中插入元素 $8$,插入过程如下:
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/1e24a1487a004ec5b2465f7fec68d034.png'></center>
|
|
|
|
|
|
(1)根据二叉搜索树的插入操作,将 $8$ 插入 $9$ 的左子节点位置,假设 $8$ 的随机数优先级为 $25016$。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/e754fab30c0f4e0cb4d499e2c1500339.png'></center>
|
|
|
|
|
|
(2)回溯时,判断是否需要旋转,$9$ 的优先级比其左子节点小,因此 $9$ 节点右旋。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/d325a7a47b5d4946b4588e177e589f55.png'></center>
|
|
|
|
|
|
|
|
|
(3)继续向上判断,$7$ 的优先级比 $7$ 的右子节点小,因此$7$节点左旋。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/4af4884bed694b979eaedd95b499298b.png'></center>
|
|
|
|
|
|
(4)继续向上判断,$6$ 的优先级不比 $6$ 的左右子节点小,满足最大堆性质,无须调整,已向上判断到树根,算法停止。
|
|
|
|
|
|
#### 删除 $remove$
|
|
|
$Treap$ 的删除操作 :找到待删除的节点,将该节点向优先级大的子节点旋转,一直旋转到叶子,直接删除叶子即可。
|
|
|
|
|
|
**算法步骤**
|
|
|
|
|
|
* 从根节点 $p$ 开始,若待删除元素 $key$ 等于 $tr[p].key$,则:
|
|
|
* 若 $p$ 只有左子树或只有右子树,则令其子树子承父业代替 $p$ ,返回;
|
|
|
* 若 $tr[p.l].val > tr[p.r].val$ ,则 $p$ 右旋,继续在 $p$ 的 **右子树中递归** 删除
|
|
|
* 若 $tr[p.l].val < tr[p.r].val$ ,则 $p$ 左旋,继续在 $p$ 的 **左子树中递归** 删除
|
|
|
|
|
|
* 若 $key < tr[p].key$,则在 $p$ 的 **左子树中递归** 删除
|
|
|
|
|
|
* 若 $key > tr[p].key$,则在 $p$ 的 **右子树中递归** 删除
|
|
|
|
|
|
在上面的树堆中删除元素 $8$,删除过程如下:
|
|
|
|
|
|
(1)根据二叉搜索树的删除操作,首先找到 $8$ 的位置,$8$ 的右子节点优先级大,$8$ 左旋。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/911d1f4ee125404e99ba457f66d33662.png'></center>
|
|
|
|
|
|
(2)接着判断,$8$ 的左子节点优先级大,$8$ 右旋。
|
|
|
<center><img src='https://img-blog.csdnimg.cn/5892b5b17d9945009ba71cb5a3ba7547.png'></center>
|
|
|
|
|
|
(3)此时 $8$ 只有一个左子树,左子树子承父业代替它
|
|
|
<center><img src='https://img-blog.csdnimg.cn/407a2a829ac74222997b7887e54e36b2.png'></center>
|
|
|
|
|
|
#### 前驱 $get\_prev$
|
|
|
在 $Treap$ 中求一个节点 $key$ 的前驱时,首先从树根开始,若当前节点的值小于 $key$,则用 $res$ 暂存该节点的值,在当前节点的右子树中寻找,否则在当前节点的左子树中寻找,直到当前节点为空,返回 $res$,即为 $key$ 的前驱。
|
|
|
|
|
|
#### 后继 $get\_next$
|
|
|
在 $Treap$ 中求一个节点 $key$ 的后继时,首先从树根开始,若当前节点的值大于 $key$,则用 $res$ 暂存该节点的值,在当前节点的左子树中寻找,否则在当前节点的右子树中寻找,直到当前节点为空,返回 $res$,即为 $key$ 的后继。
|
|
|
|
|
|
### 三、实现代码
|
|
|

|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
#include <cstdio>
|
|
|
#include <cstring>
|
|
|
#include <iostream>
|
|
|
#include <algorithm>
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
|
const int N = 100010, INF = 1e8;
|
|
|
|
|
|
int n;
|
|
|
struct Node {
|
|
|
int l, r; // 左右儿子节点号
|
|
|
int key, val; // BST中的真实值,堆中随机值
|
|
|
int cnt, size; // 当前数字个数,小于等于当前数字的数字个数总和
|
|
|
} tr[N];
|
|
|
|
|
|
int root, idx;
|
|
|
|
|
|
void pushup(int p) {
|
|
|
tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt; // BST左子树数字个数+右子树数字个数+自己数字个数
|
|
|
}
|
|
|
|
|
|
int get_node(int key) {
|
|
|
tr[++idx].key = key; //填充 BST的值
|
|
|
tr[idx].val = rand(); //堆中的随机值
|
|
|
tr[idx].cnt = tr[idx].size = 1;
|
|
|
return idx;
|
|
|
}
|
|
|
|
|
|
//右旋
|
|
|
void zig(int &p) {
|
|
|
int q = tr[p].l;
|
|
|
tr[p].l = tr[q].r;
|
|
|
tr[q].r = p;
|
|
|
p = q;
|
|
|
pushup(tr[p].r), pushup(p);
|
|
|
}
|
|
|
|
|
|
//左旋
|
|
|
void zag(int &p) {
|
|
|
int q = tr[p].r;
|
|
|
tr[p].r = tr[q].l;
|
|
|
tr[q].l = p;
|
|
|
p = q;
|
|
|
pushup(tr[p].l), pushup(p);
|
|
|
}
|
|
|
|
|
|
void build() {
|
|
|
get_node(-INF), get_node(INF);
|
|
|
root = 1, tr[1].r = 2;
|
|
|
pushup(root);
|
|
|
if (tr[1].val < tr[2].val) zag(root);
|
|
|
}
|
|
|
|
|
|
void insert(int &p, int key) {
|
|
|
if (!p)
|
|
|
p = get_node(key);
|
|
|
else if (tr[p].key == key)
|
|
|
tr[p].cnt++;
|
|
|
else if (tr[p].key > key) {
|
|
|
insert(tr[p].l, key); //往左边插入
|
|
|
if (tr[tr[p].l].val > tr[p].val) zig(p); //左儿子大,右旋
|
|
|
} else {
|
|
|
insert(tr[p].r, key); //往右边插入
|
|
|
if (tr[tr[p].r].val > tr[p].val) zag(p); //右儿子大,左旋
|
|
|
}
|
|
|
pushup(p);
|
|
|
}
|
|
|
|
|
|
void remove(int &p, int key) {
|
|
|
if (!p) return; //如果发现p==0, 就是没找着
|
|
|
|
|
|
if (tr[p].key == key) { //如果找着了
|
|
|
if (tr[p].cnt > 1) //并且不止1个,这个就简单了,去掉一个就行了,记得 pushup
|
|
|
tr[p].cnt--;
|
|
|
else if (tr[p].l || tr[p].r) { //如果只有1个,并且,有左儿子或右儿子,这时不能直接删除掉,需要处理一下
|
|
|
if (!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val) { //如果没有右儿子,或者是左儿子的随机值大于右儿子随机值,右旋
|
|
|
zig(p); //右旋后,此值向右运动,继续递归右子树处理
|
|
|
remove(tr[p].r, key);
|
|
|
} else {
|
|
|
zag(p); //左旋,此值向左运动,继续递归向左子树处理
|
|
|
remove(tr[p].l, key);
|
|
|
}
|
|
|
} else
|
|
|
p = 0; //左右都没有子树,直接标识为删除
|
|
|
} else if (tr[p].key > key) //如果在左
|
|
|
remove(tr[p].l, key);
|
|
|
else
|
|
|
remove(tr[p].r, key); //如果在右
|
|
|
|
|
|
//向上更新统计信息
|
|
|
pushup(p);
|
|
|
}
|
|
|
|
|
|
int get_rank(int p, int key) { // 通过数值找排名
|
|
|
if (!p) return 0; // 本题中不会发生此情况
|
|
|
if (tr[p].key == key) return tr[tr[p].l].size + 1;
|
|
|
if (tr[p].key > key) return get_rank(tr[p].l, key);
|
|
|
return tr[tr[p].l].size + tr[p].cnt + get_rank(tr[p].r, key);
|
|
|
}
|
|
|
|
|
|
int get_key(int p, int rank) { // 通过排名找数值
|
|
|
if (!p) return INF; // 本题中不会发生此情况
|
|
|
if (tr[tr[p].l].size >= rank) return get_key(tr[p].l, rank);
|
|
|
if (tr[tr[p].l].size + tr[p].cnt >= rank) return tr[p].key;
|
|
|
return get_key(tr[p].r, rank - tr[tr[p].l].size - tr[p].cnt);
|
|
|
}
|
|
|
|
|
|
int get_prev(int p, int key) { // 找到严格小于key的最大数
|
|
|
if (!p) return -INF;
|
|
|
if (tr[p].key >= key) return get_prev(tr[p].l, key);
|
|
|
return max(tr[p].key, get_prev(tr[p].r, key)); //当前位置可能成为答案
|
|
|
}
|
|
|
|
|
|
int get_next(int p, int key) { // 找到严格大于key的最小数
|
|
|
if (!p) return INF;
|
|
|
if (tr[p].key <= key) return get_next(tr[p].r, key);
|
|
|
return min(tr[p].key, get_next(tr[p].l, key));
|
|
|
}
|
|
|
|
|
|
int main() {
|
|
|
//加快读入
|
|
|
ios::sync_with_stdio(false), cin.tie(0);
|
|
|
|
|
|
build();
|
|
|
|
|
|
cin >> n;
|
|
|
while (n--) {
|
|
|
int op, x;
|
|
|
cin >> op >> x;
|
|
|
if (op == 1)
|
|
|
insert(root, x);
|
|
|
else if (op == 2)
|
|
|
remove(root, x);
|
|
|
else if (op == 3)
|
|
|
printf("%d\n", get_rank(root, x) - 1);
|
|
|
else if (op == 4)
|
|
|
printf("%d\n", get_key(root, x + 1));
|
|
|
else if (op == 5)
|
|
|
printf("%d\n", get_prev(root, x));
|
|
|
else
|
|
|
printf("%d\n", get_next(root, x));
|
|
|
}
|
|
|
return 0;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 四、$fhq$ 树堆
|
|
|
|
|
|
$FHQ$ $Treap$,以下简写为$fhq$,是一种$treap$(树堆)的变体,功能比$treap$强大,代码比$splay$好写,易于理解,常数稍大.
|
|
|
|
|
|
$fhq$ **不需要** 通过一般平衡树的 **左右旋转** 来保持平衡,而是通过 **分裂$split$** 和 **合并$merge$** 来实现操作。
|
|
|
|
|
|
|
|
|
#### 结构
|
|
|
以结构体作为树的每一个节点,存储:
|
|
|
* ① 左子树位置
|
|
|
* ② 右子树的位置
|
|
|
* ③ 权值$key$
|
|
|
* ④ 堆中随机索引$val$
|
|
|
* ⑤ 子树大小.一般子树大小用于查排名和查值
|
|
|
|
|
|
`root`是树根编号,`idx`是点的编号.
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
struct Node {
|
|
|
int l, r; // 左右子树编号
|
|
|
int key, val; // key权 val堆权
|
|
|
int size; // 子树大小
|
|
|
} tr[N];
|
|
|
int root, idx;
|
|
|
```
|
|
|
|
|
|
`fhq`和`treap`一样满足`treap`的性质,也就是 **既是$BST$**,又是 **随机权值的堆**.
|
|
|
|
|
|
至于为什么满足堆的性质的$BST$就能平衡,有如下定理保证:
|
|
|
> 一颗有$n$个不同关键字随机构建的$BST$的期望高度为$logn$.
|
|
|
|
|
|
随机堆的权值正是模拟了随机构建$BST$,所以$treap$是平衡的,同理$fhq$也平衡.
|
|
|
|
|
|
#### 创建节点和更新子树大小
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
int get_node(int key) {
|
|
|
tr[++idx].key = key;
|
|
|
tr[idx].val = rnd();
|
|
|
tr[idx].size = 1;
|
|
|
return idx;
|
|
|
}
|
|
|
void pushup(int p) {
|
|
|
tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + 1;
|
|
|
}
|
|
|
```
|
|
|
子树大小由两侧子树和根节点更新.`rnd()`为随机值产生函数。
|
|
|
|
|
|
首先$fhq-treap$是一个二叉搜索树($BST$),它的每个节点有两个主要信息:$key$和$val$,$key$是我们$fhq-treap$要维护的键值,而$val$是随机生成的$(rand())$,$key$信息主要用于我们对于题目信息的处理,而$val$则是用于维持$fhq-treap$在结构上满足期望高度为$O(logn)$的。$fhq-treap$ 除了要满足关于$key$的$BST$性质之外,还需满足关于$val$的 **小根堆** 性质。就是说,对于任意$fhq-treap$中的节点 $i$ :
|
|
|
|
|
|
* 其左子树上的所有节点的$key$ **小于等于** $i$ 节点的$key$值,$i$ 节点所有右子树上所有节点的$key$ **大于等于** $i$ 节点的$key$值
|
|
|
|
|
|
* 对于任意节点 $i$ ,其左、右儿子的$val$值 **大于等于** $i$ 的$val$值(满足 **小根堆** 的性质)
|
|
|
|
|
|
要牢牢记住这两点的区别,否则会像我一样搞混,然后写代码的时候狂 $WA$,。下面给出一颗$fhq-treap$:
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/7118c748ee414ebea5f72e1e3a0d93c2.png'></center>
|
|
|
|
|
|
上图就是一颗$fhq-treap$,其中每个节点中的数字为$val$,每个节点下方的数字为当前节点的$key$。可以发现一颗$fhq-treap$中序遍历后得到的序列是单调递增。 这里,我们要引入$fhq-treap$的两个基本操作:$split$和$merge$, 其中$split$是分离操作,$merge$是合并操作,下面我们来一一阐明:
|
|
|
|
|
|
**1. $split$操作**
|
|
|
|
|
|
通常情况下,$split$ 用于分离一颗$fhq-treap$,对于一个元素 $x$ ,我们会将这棵$fhq-treap$分裂为左右两棵树,左树上每个节点的$key$值都小于等于$(<=)x$ , 而右树上的所有节点的$key$值都大于 $x$ ;下面给出代码:
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
//将以p为根的平衡树进行分裂,小于等于key的都放到以x为根的子树中,大于key放到以y为根的子树中
|
|
|
void split(int p, int key, int &x, int &y) {
|
|
|
if (!p) { //当前节点为空
|
|
|
x = y = 0;
|
|
|
return;
|
|
|
}
|
|
|
if (tr[p].key <= key)
|
|
|
x = p, split(tr[p].r, key, tr[p].r, y); // x确定了,左边确定了,但右边未确定,需要继续递归探索
|
|
|
else
|
|
|
y = p, split(tr[p].l, key, x, tr[p].l); // y确定了,右边确定了,但左边未确定,需要继续递归探索
|
|
|
|
|
|
pushup(p); //更新统计信息
|
|
|
}
|
|
|
```
|
|
|
以上代码如果学过线段树的话会好理解的多,本蒟蒻也建议先去熟练掌握线段树再来学习$fhq-treap$,这样会轻松很多。下面我们来模拟这个过程—— **本蒟蒻就是开始没有自己去模拟,导致一直不是很理解$fhq-treap$的实现方式**。
|
|
|
|
|
|
对于上图,假如我们要将其分离为小于等于$18$和大于$18$两个部分:
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/4b0e2dfece2a4675b3150f5fc82365df.png'></center>
|
|
|
|
|
|
经过此役,$x$的指向已经很清楚了,就是$p=G$,因为$G$左侧的所有数,都会比$G$小,所以左子树的范围也就相应继承下来了:$tr[p].l$不需要动,但$x$的右边界不没有确定,因为$18$到底应该在右子树的哪个位置割开呢?还需要继续研究,继续递归解决。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/48a4396f28ab4bb8be185a42eb887e19.png'></center>
|
|
|
|
|
|
经过此役,$x$ 的右边界还在不断的修改,不断的向右逼近。
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/0b39d16eccf645778db2bbf268db6114.png'></center>
|
|
|
|
|
|
经过此役,出现了第一个大于$key$的点,那么此点做为一个有代表性的点,被设置为$y$。同时,$x$的右边界,$y$的左边界依然不明,需要继续递归查找。分裂$K$节点的左子树,由于$I$节点的权值$20>18$,所以我们将 $I$ 节点归入右$fhq-treap$,接下来我们会碰到空节点,递归将会跳出,这时,我们就能得到左右两棵$fhq-treap$了:
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/d0822bff62ad49fc8f94381c6d0b0dbb.png'></center>
|
|
|
|
|
|
以上就是$fhq-treap$的基本操作之一$split$操作的全部过程了 。
|
|
|
|
|
|
|
|
|
**$2$. $merge$ 操作**
|
|
|
这是一个合并操作,但它也不能随便合并,如果要合并两棵$fhq-treap$,那么它们可以进行合并当且仅当左$fhq-treap$上所有节点的$key$值都小于等于右$fhq-treap$中的最小$key$值(也就是左树小于等于右树)或两棵树中有空树。 这个性质很重要,为根据$val$合并两棵树做好的前提,保证了$BST$结构不会因合并而被破坏。 合并时,我们会根据左右两棵树的根节点的$val$值大小进行合并,大家先看一下代码:
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
//将以x,y为根的两个子树合并成一棵树.要求x子树中所有key必须小于等于y子树中所有key
|
|
|
int merge(int x, int y) {
|
|
|
if (!x || !y) return x + y; //如果x或者y有一个是空了,那么返回另一个即可
|
|
|
|
|
|
int p; //根,返回值
|
|
|
if (tr[x].val > tr[y].val) { // x.key<y.key,并且, tr[x].val > tr[y].val, x在y的左上,此时理解为大根堆,y向x的右下角合并
|
|
|
p = x;
|
|
|
tr[x].r = merge(tr[x].r, y);
|
|
|
} else {
|
|
|
p = y;
|
|
|
tr[y].l = merge(x, tr[y].l); //复读机
|
|
|
}
|
|
|
|
|
|
pushup(p); //更新统计信息
|
|
|
return p;
|
|
|
}
|
|
|
```
|
|
|
还是一样,画图好理解:
|
|
|
<center><img src='https://img-blog.csdnimg.cn/75fd83d244ce4ff68cb17fd770bc950b.png'></center>
|
|
|
|
|
|
以上是两棵要合并的$fhq-treap$
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/1c1e1ec81d574421b5543d13fa879ee8.png'></center>
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/2228834479c8400190c0bb8eaba960ad.png'></center>
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/09728d95e9aa498996d04d416e2ada66.png'></center>
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/1d9aa7e35e9346ae8e3c838e4fdb62b5.png'></center>
|
|
|
|
|
|
<center><img src='https://img-blog.csdnimg.cn/71b69207eca64aeaa7b8866be2df36f1.png'></center>
|
|
|
|
|
|
以上就是$fhq-treap$合并的全部过程了。
|
|
|
|
|
|
### 六、实现代码
|
|
|
```cpp {.line-numbers}
|
|
|
#include <cstdio>
|
|
|
#include <cstring>
|
|
|
#include <algorithm>
|
|
|
#include <iostream>
|
|
|
|
|
|
//普通Treap 316 ms
|
|
|
// FHQ Treap 433 ms
|
|
|
//两者基本在一个数量级上,常数略大
|
|
|
|
|
|
using namespace std;
|
|
|
const int INF = 0x3f3f3f3f;
|
|
|
const int N = 1e5 + 10;
|
|
|
|
|
|
struct Node {
|
|
|
int l, r; //左右儿子的节点号
|
|
|
int key; // BST中的真实值
|
|
|
int val; //堆中随机值,用于防止链条化
|
|
|
int size; //小于等于 key的数字个数,用于计算rank等属性
|
|
|
} tr[N];
|
|
|
|
|
|
int root, idx; //用于动态开点,配合tr记录FHQ Treap使用
|
|
|
int x, y, z; //本题用的三个临时顶点号
|
|
|
|
|
|
void pushup(int p) {
|
|
|
tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + 1; //合并信息
|
|
|
}
|
|
|
|
|
|
int get_node(int key) { //创建一个新节点
|
|
|
tr[++idx].key = key; //创建一个新点,值为key
|
|
|
tr[idx].val = rand(); //随机一个堆中索引号
|
|
|
tr[idx].size = 1; //新点,所以小于等于它的个数为1个,只有自己
|
|
|
return idx; //返回点号
|
|
|
}
|
|
|
|
|
|
//将以p为根的平衡树进行分裂,小于等于key的都放到以x为根的子树中,大于key放到以y为根的子树中
|
|
|
void split(int p, int key, int &x, int &y) {
|
|
|
if (!p) { //当前节点为空
|
|
|
x = y = 0;
|
|
|
return;
|
|
|
}
|
|
|
if (tr[p].key <= key)
|
|
|
x = p, split(tr[p].r, key, tr[p].r, y); // x确定了,左边确定了,但右边未确定,需要继续递归探索
|
|
|
else
|
|
|
y = p, split(tr[p].l, key, x, tr[p].l); // y确定了,右边确定了,但左边未确定,需要继续递归探索
|
|
|
|
|
|
pushup(p); //更新统计信息
|
|
|
}
|
|
|
|
|
|
//将以x,y为根的两个子树合并成一棵树.要求x子树中所有key必须小于等于y子树中所有key
|
|
|
int merge(int x, int y) {
|
|
|
if (!x || !y) return x + y; //如果x或者y有一个是空了,那么返回另一个即可
|
|
|
|
|
|
int p; //根,返回值
|
|
|
if (tr[x].val > tr[y].val) { // x.key<y.key,并且, tr[x].val > tr[y].val, x在y的左上,此时理解为大根堆,y向x的右下角合并
|
|
|
p = x;
|
|
|
tr[x].r = merge(tr[x].r, y);
|
|
|
} else {
|
|
|
p = y;
|
|
|
tr[y].l = merge(x, tr[y].l); //复读机
|
|
|
}
|
|
|
|
|
|
pushup(p); //更新统计信息
|
|
|
return p;
|
|
|
}
|
|
|
|
|
|
void insert(int key) {
|
|
|
split(root, key, x, y); //按k分割
|
|
|
root = merge(merge(x, get_node(key)), y); //在x与key节点合并,再与key合并
|
|
|
}
|
|
|
|
|
|
void remove(int key) {
|
|
|
split(root, key, x, z);
|
|
|
split(x, key - 1, x, y);
|
|
|
// x<=key ,再分x <= key - 1,y就是=key的树
|
|
|
y = merge(tr[y].l, tr[y].r); //删除y点(根)
|
|
|
root = merge(merge(x, y), z); //合并x,y,z
|
|
|
}
|
|
|
|
|
|
int get_rank(int key) { //按值查排名
|
|
|
split(root, key - 1, x, y); //按key-1分割,x子树大小+1就是排名
|
|
|
int rnk = tr[x].size + 1; //储存x的大小+1
|
|
|
root = merge(x, y);
|
|
|
return rnk;
|
|
|
}
|
|
|
|
|
|
int get_key(int rnk) { //按排名查值
|
|
|
int p = root;
|
|
|
while (p) {
|
|
|
if (tr[tr[p].l].size + 1 == rnk)
|
|
|
break; //找到排名了
|
|
|
else if (tr[tr[p].l].size >= rnk)
|
|
|
p = tr[p].l; //当前size>=rank,去左子树
|
|
|
else {
|
|
|
//去右子树中找rank -= 左子树大小+1(根)的排名
|
|
|
rnk -= tr[tr[p].l].size + 1;
|
|
|
p = tr[p].r;
|
|
|
}
|
|
|
}
|
|
|
return tr[p].key;
|
|
|
}
|
|
|
|
|
|
//返回<key的最大数
|
|
|
int get_prev(int key) {
|
|
|
split(root, key - 1, x, y); //按key-1分,x最右节点就是前驱
|
|
|
int p = x;
|
|
|
while (tr[p].r) p = tr[p].r; //向右走
|
|
|
int res = tr[p].key;
|
|
|
root = merge(x, y);
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
//返回>key的最小数
|
|
|
int get_next(int key) {
|
|
|
split(root, key, x, y); //按key分y最左节点是后继
|
|
|
int p = y;
|
|
|
while (tr[p].l) p = tr[p].l;
|
|
|
int res = tr[p].key;
|
|
|
root = merge(x, y);
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
//创建FHQ Treap带哨兵的空树
|
|
|
void build() {
|
|
|
get_node(-INF), get_node(INF);
|
|
|
root = 1, tr[1].r = 2; //+inf > -inf,+inf在-inf右边
|
|
|
pushup(root); //更新root的size
|
|
|
}
|
|
|
|
|
|
int main() {
|
|
|
//加快读入
|
|
|
ios::sync_with_stdio(false), cin.tie(0);
|
|
|
//事实证明,套路很重要
|
|
|
build();
|
|
|
|
|
|
int q;
|
|
|
cin >> q;
|
|
|
while (q--) {
|
|
|
int op, x;
|
|
|
cin >> op >> x;
|
|
|
if (op == 1)
|
|
|
insert(x);
|
|
|
else if (op == 2)
|
|
|
remove(x);
|
|
|
else if (op == 3)
|
|
|
printf("%d\n", get_rank(x) - 1); //因为前面多了-INF哨兵,所以排名还需要减1
|
|
|
else if (op == 4)
|
|
|
printf("%d\n", get_key(x + 1)); //本来需要查询排名为x的,现在由于增加了一个左哨兵,就需要查询x+1位的
|
|
|
else if (op == 5)
|
|
|
printf("%d\n", get_prev(x));
|
|
|
else if (op == 6)
|
|
|
printf("%d\n", get_next(x));
|
|
|
}
|
|
|
return 0;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 七、待研习
|
|
|
[ 平衡树进阶博文](https://blog.csdn.net/AKAPinkman/article/details/118079835)
|
|
|
[【模板】普通平衡树 - 洛谷](https://www.luogu.com.cn/problem/P3369)
|
|
|
[【模板】文艺平衡树 - 洛谷](https://www.luogu.com.cn/problem/P3391)
|
|
|
[ AcWing 266. 超级备忘录 - AcWing题库](https://www.acwing.com/problem/content/268/)
|
|
|
[ [NOI2005] 维护数列 - 洛谷](https://www.luogu.com.cn/problem/P2042)
|
|
|
[【模板】可持久化平衡树 - 洛谷](https://www.luogu.com.cn/problem/P3835)
|
|
|
[【模板】可持久化文艺平衡树 - 洛谷](https://www.luogu.com.cn/problem/P5055)
|
|
|
[【模板】二逼平衡树(树套树)](https://www.luogu.com.cn/problem/P3380)
|
|
|
[FHQ-Treap(非旋treap/平衡树)——从入门到入坟](https://blog.csdn.net/yyh_getAC/article/details/125710091) |