1. 基本知识
1.1 Treap
Treap(树堆) 是一种弱平衡的二叉搜索树,每个节点的权值具有二叉搜索树的性质,优先级具有堆的性质
具体来说,树堆满足如下性质
- 左子结点的权值小于根节点的权值
- 右子节点的权值大于根节点的权值
- 子节点的优先级(注意,不是权值!)都大于/都小于根节点的优先级(取决于大根堆/小根堆)
如下图即为一个 Treap 的例子(采用小根堆)——
注:黑色数字为权值,棕色数字为优先级
为什么相较于 BST,需要对每个节点多维护一个优先级?
对每个节点,维护满足堆性质的优先级,是为了保证树尽量平衡:
对于一棵 每层节点较多 的二叉搜索树,查找时需要递归的次数便较小,复杂度为
反之,退化成一条链 是二叉搜索树的最坏情况,此时查找所需复杂度为线性,而优先级的设置便防止了这种情况的出现
1.2 FHQ-Treap
FHQ-Treap 又称分裂合并 Treap,无须旋转,只需支持分裂和合并两种基本操作
FHQ-Treap 支持可持久化和区间操作,但此处仅介绍基本操作
2. 初始化 FHQ-Treap
在 FHQ-Treap 的节点上维护如下属性与方法(采用指针式):
- 左右子节点的地址
- 当前节点的权值
- 当前节点的优先级
- 当前节点的出现次数
- 当前节点的子树大小
- 重新计算子树大小的方法
于是可以得到节点的代码:
struct Node {
Node *ch[2]; // 左右孩子的指针
int val; // 权值
int rank; // 优先级
int rep_cnt; // 出现次数
int siz; // 子树大小
Node (int _val) : val(_val), rep_cnt(1), siz(1) { // 构造函数
ch[0] = ch[1] = nullptr;
rank = rand(); // 随机取优先级
}
void upd_siz() { // 重新计算子树大小的方法
siz = rep_cnt;
if (ch[0] != nullptr) {
siz = siz + ch[0]->siz; // 加上左子树的大小
}
if (ch[1] != nullptr) {
siz = siz + ch[1]->siz; // 加上右子树的大小
}
}
};
3. 分裂
3.1 按值分裂
分裂过程接收 根节点的指针 及 关键值 两个参数,返回两个指针,分别指向分裂后的 两个 Treap,第一个 Treap 所有节点的值小于等于关键值,第二个 Treap 所有节点的值大于关键值
对于要分裂的关键值 (),大致分裂思路如下:
- 首先比较根节点的值与关键值
- 如果根节点的值小于等于关键值,说明根节点及其左子树上的所有节点均小于等于关键值,而此时根节点的右子树上仍可能有节点小于等于关键值,因此递归分裂右子树 (结果记为
),再将当前根节点的右孩子指向
,最终返回根节点的指针与
- 如果根节点的值大于关键值,说明根节点及其右子树上的所有节点均大于关键值,而此时根节点的左子树上仍可能有节点大于关键值,因此递归分裂左子树 (结果记为
),再将当前根节点的左孩子指向
,最终返回
与根节点的指针
于是可以得到按值分裂的代码:
pair<Node *, Node *> split(Node *root, int key) { // 根节点指针,关键值
if (root == nullptr) { // 当前节点不存在
return {nullptr, nullptr};
}
if (root->val <= key) { // 根节点的值小于等于关键值
auto temp = split(root->ch[1], key); // 递归分裂右子树
root->ch[1] = temp.first;
root->upd_siz(); // 别忘了更新子树大小
return {root, temp.second};
} else { // 根节点的值大于关键值
auto temp = split(root->ch[0], key); // 递归分裂左子树
root->ch[0] = temp.second;
root->upd_siz(); // 别忘了更新子树大小
return {temp.first, root};
}
}
3.2 按排名分裂
排名定义为,比当前权值小的权值的个数 +1(注意重复的也算哦)
分裂过程接收 根节点的指针 及 排名 两个参数,返回三个指针,分别指向分裂后的 三个 Treap,第一个 Treap 所有节点的排名小于给出排名,第二个 Treap 所有节点的排名等于给出排名(只可能有一个节点或没有节点),第三个 Treap 所有节点的排名大于给出排名
对于要分裂的排名 (),大致分裂思路如下:
- 首先注意到当前节点的排名必大于等于 左子树的大小,因此先比较左子树大小与当前排名
- 如果左子树大小大于等于当前排名,说明根节点及其右子树上所有节点的排名均大于等于当前排名,而此时根节点的左子树上仍可能有节点的排名大于当前排名,因此递归分裂左子树 (结果记为
),再将当前根节点的左孩子指向
,最终返回
与当前根节点的指针
- 如果左子树大小小于当前排名,注意到当前节点右子树上节点的排名必大于等于 左子树大小+当前节点出现次数,因此再次比较 左子树大小+当前节点出现次数 与 当前排名
- 如果 左子树大小+当前节点出现次数大于等于当前排名,又因为左子树大小大于当前排名,因此当前节点即为要找的节点,直接返回 当前节点左子树的指针,当前节点指针与当前节点右子树的指针
- 如果 左子树大小+当前节点出现次数小于当前排名,说明根节点及其左子树上所有节点的排名均小于当前排名,而此时根节点的右子树上仍可能有节点的排名小于当前排名,因此递归分裂右子树(注意更新目标权值!) (结果记为
),再将当前根节点的右孩子指向
,最终返回当前根节点的指针与
于是可以得到按排名分裂的代码:
tuple<Node *, Node *, Node *> split_by_rk(Node *root, int rk) { // 根节点指针,排名
if (root == nullptr) { // 当前节点不存在
return {nullptr, nullptr, nullptr};
}
int ls_siz; // 左子树的大小
if (root->ch[0] == nullptr) { // 当前节点没有左子树
ls_siz = 0;
} else {
ls_siz = root->ch[0]->siz;
}
if (rk <= ls_siz) { // 左子树的大小 大于等于 当前排名
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(root->ch[0], rk); // 递归分裂左子树
root->ch[0] = r;
root->upd_siz(); // 别忘了更新子树大小
return {l, mid, root};
} else if (rk <= ls_siz + root->rep_cnt) { // 左子树的大小+当前节点的出现次数 大于等于 当前排名
Node *lt = root->ch[0];
Node *rt = root->ch[1];
root->ch[0] = root->ch[1] = nullptr;
return {lt, root, rt}; // 直接返回
} else { // 左子树的大小+当前节点的出现次数 小于 当前排名
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(root->ch[1], rk - ls_siz - root->rep_cnt); // 注意更新目标权值!
root->ch[1] = l;
root->upd_siz(); // 别忘了更新子树大小
return {root, mid, r};
}
}
4. 合并
合并过程接收 左Treap根节点的指针 及 右Treap根节点的指针,满足左Treap所有节点的值 小于等于 右Treap所有节点的值,返回合并后的 Treap 根节点的指针
由于合并过程需要注意 优先级满足堆的性质,因此主要考虑两根节点间优先级的关系
对于要合并的根节点 (),大致合并思路如下(小根堆):
- 首先判断
是否存在,若都不存在返回
,若只有一个存在则返回存在的根节点的指针
- 如果
的优先级 小于
的优先级,则将
作为新Treap的根节点,递归合并
的右子树 与
,并将
的右子树 指向 合并结果
- 如果
的优先级 大于等于
的优先级,则将
作为新Treap的根节点,递归合并
的左子树 与
,并将
的左子树 指向 合并结果
于是可以得到合并的代码:
Node *merge(Node *u, Node *v) { // 左Treap根节点的指针,右Treap根节点的指针
if (u == nullptr && v == nullptr) { // 都不存在
return nullptr;
}
if (u != nullptr && v == nullptr) { // 只有v存在
return u;
}
if (v != nullptr && u == nullptr) { // 只有u的存在
return v;
}
if (u->rank < v->rank) { // u的优先级 小于 v的优先级
u->ch[1] = merge(u->ch[1], v); // 递归合并
u->upd_siz(); // 别忘了更新大小
return u;
} else { // u的优先级 大于等于 v的优先级
v->ch[0] = merge(u, v->ch[0]); // 递归合并
v->upd_siz(); // 别忘了更新大小
return v;
}
}
5. 插入节点
我们知道,由分裂操作的性质,值小于等于 的节点会被分到第一个 Treap
由这一性质,大致插入思路如下:
- 按照
分裂当前 Treap,则会产生
两棵 Treap,满足
- 按照
分裂
,则会再次产生
两颗 Treap,满足
- 此时若
中含有节点,则其权值必等于
,将其 出现次数+1 即可
- 若
中不含有节点,则创建一个权值为
的新节点
- 最后按顺序将所有分裂出的 Treap 合并回去即可
于是可以得到插入节点的代码:
void insert(int val) { // 要插入节点的权值
auto temp = split(root, val); // 按照 val 分裂
auto l_tr = split(temp.first, val - 1); // 再次按照 val-1 分裂
Node *new_node;
if (l_tr.second == nullptr) { // 原 Treap 中没有权值为 val 的节点
new_node = new Node(val); // 创建权值为 val 的新节点
} else { // 原 Treap 中有权值为 val 的节点
l_tr.second->rep_cnt++;
l_tr.second->upd_siz(); // 别忘了更新子树大小
}
Node *l_tr_c;
if (l_tr.second == nullptr) {
l_tr_c = merge(l_tr.first, new_node);
} else {
l_tr_c = merge(l_tr.first, l_tr.second);
}
root = merge(l_tr_c, temp.second); // 最后合并回来 (root 为整个 Treap 的根节点指针)
}
6. 删除节点
删除节点使用与插入节点相似的方法,具体参见代码注释
void del(int val) { // 要删除节点的权值
auto temp = split(root, val); // 按照 val 分裂
auto l_tr = split(temp.first, val - 1); // 再次按照 val-1 分裂
if (l_tr.second->rep_cnt > 1) { // 如果要删除节点的出现次数大于1
l_tr.second->rep_cnt--; // 将出现次数减去1
l_tr.second->upd_siz(); // 别忘了更新子树大小
l_tr.first = merge(l_tr.first, l_tr.second); // 最后合并回来
} else {
if (temp.first == l_tr.second) { // 如果 temp 的左子树只有 要删除节点 这一个节点
temp.first = nullptr; // 标记左子树为 nullptr 表示删除
}
delete l_tr.second; // 删除指针
l_tr.second = nullptr;
}
root = merge(l_tr.first, temp.second); // 最后合并回来
}
7. 根据值查询排名
根据 -1 分裂 Treap,分裂后的第一个 Treap 的大小即为所求
int query_rank_by_val(Node *root, int val) { // 当前根节点,要查询排名的权值
auto temp = split(root, val - 1); // 按照 val-1 分裂
int res;
if (temp.first == nullptr) { // 没有比 val 小的节点
res = 1;
} else {
res = temp.first->siz + 1; // 返回第一个 Treap 的大小加上1 (参见排名的定义)
}
root = merge(temp.first, temp.second); // 最后合并回去
return res;
}
8. 根据排名查询值
根据 分裂 Treap (调用
函数),第二个 Treap 的值即为所求
int query_val_by_rank(Node *root, int rk) { // 当前根节点,要查询权值的排名
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(root, rk); // 按照 rk 分裂
if (mid == nullptr){ // 没有排名为 rk 的节点 (rk 大于整个 Treap 的大小)
return 2147483647;
}
int res = mid->val; // 返回第二个 Treap 的值
root = merge(merge(l, mid), r); // 最后合并回去
return res;
}
9. 查询给定值的前驱
前驱定义为,小于 但最大的数
问题相当于在比 小的所有节点中找出排名最大的节点
大致查前驱思路:
- 按照
分裂 Treap
- 在分裂后的第一个 Treap 中寻找 rk 为第一个 Treap 的大小的节点,其值即为所求
于是可以得到查前驱的代码:
int query_pre(int val) { // 要查前驱的权值
auto temp = split(root, val - 1); // 按照 val-1 分裂
if (temp.first == nullptr){ // 前驱不存在
return -2147483647;
}
int res = query_val_by_rank(temp.first, temp.first->siz);
root = merge(temp.first, temp.second); // 最后合并回去
return res;
}
10. 查询给定值的后继
后继定义为,大于 但最小的数
与查前驱类似,问题相当于在比 大的所有节点中找出排名最小的节点 (其实就是为 1)
大致查后继思路:
- 按照
分裂 Treap
- 在分裂后的第二个 Treap 中寻找 rk 为 1 的节点,其值即为所求
于是可以得到查后继的代码:
int query_nxt(int val) { // 要查后继的权值
auto temp = split(root, val); // 按照 val 分裂
if (temp.second == nullptr){ // 后继不存在
return 2147483647;
}
int res = query_val_by_rank(temp.second, 1);
root = merge(temp.first, temp.second); // 最后合并回来
return res;
}
11. 练习题目
相信你已经掌握 FHQ-Treap 的基本操作了,那么做道板子题练练手吧~
另附上此题的 AC 代码,作为本篇笔记的代码汇总
#include <bits/stdc++.h>
using namespace std;
struct Node {
Node *ch[2];
int val;
int rank;
int rep_cnt;
int siz;
Node (int _val) : val(_val), rep_cnt(1), siz(1) {
ch[0] = ch[1] = nullptr;
rank = rand();
}
void upd_siz() {
siz = rep_cnt;
if (ch[0] != nullptr) {
siz = siz + ch[0]->siz;
}
if (ch[1] != nullptr) {
siz = siz + ch[1]->siz;
}
}
};
struct Nrot_Treap {
Node *root;
pair<Node *, Node *> split(Node *root, int key) {
if (root == nullptr) {
return {nullptr, nullptr};
}
if (root->val <= key) {
auto temp = split(root->ch[1], key);
root->ch[1] = temp.first;
root->upd_siz();
return {root, temp.second};
} else {
auto temp = split(root->ch[0], key);
root->ch[0] = temp.second;
root->upd_siz();
return {temp.first, root};
}
}
tuple<Node *, Node *, Node *> split_by_rk(Node *root, int rk) {
if (root == nullptr) {
return {nullptr, nullptr, nullptr};
}
int ls_siz;
if (root->ch[0] == nullptr) {
ls_siz = 0;
} else {
ls_siz = root->ch[0]->siz;
}
if (rk <= ls_siz) {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(root->ch[0], rk);
root->ch[0] = r;
root->upd_siz();
return {l, mid, root};
} else if (rk <= ls_siz + root->rep_cnt) {
Node *lt = root->ch[0];
Node *rt = root->ch[1];
root->ch[0] = root->ch[1] = nullptr;
return {lt, root, rt};
} else {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(root->ch[1], rk - ls_siz - root->rep_cnt);
root->ch[1] = l;
root->upd_siz();
return {root, mid, r};
}
}
Node *merge(Node *u, Node *v) {
if (u == nullptr && v == nullptr) {
return nullptr;
}
if (u != nullptr && v == nullptr) {
return u;
}
if (v != nullptr && u == nullptr) {
return v;
}
if (u->rank < v->rank) {
u->ch[1] = merge(u->ch[1], v);
u->upd_siz();
return u;
} else {
v->ch[0] = merge(u, v->ch[0]);
v->upd_siz();
return v;
}
}
void insert(int val) {
auto temp = split(root, val);
auto l_tr = split(temp.first, val - 1);
Node *new_node;
if (l_tr.second == nullptr) {
new_node = new Node(val);
} else {
l_tr.second->rep_cnt++;
l_tr.second->upd_siz();
}
Node *l_tr_c;
if (l_tr.second == nullptr) {
l_tr_c = merge(l_tr.first, new_node);
} else {
l_tr_c = merge(l_tr.first, l_tr.second);
}
root = merge(l_tr_c, temp.second);
}
void del(int val) {
auto temp = split(root, val);
auto l_tr = split(temp.first, val - 1);
if (l_tr.second->rep_cnt > 1) {
l_tr.second->rep_cnt--;
l_tr.second->upd_siz();
l_tr.first = merge(l_tr.first, l_tr.second);
} else {
if (temp.first == l_tr.second) {
temp.first = nullptr;
}
delete l_tr.second;
l_tr.second = nullptr;
}
root = merge(l_tr.first, temp.second);
}
int query_rank_by_val(Node *root, int val) {
auto temp = split(root, val - 1);
int res;
if (temp.first == nullptr) {
res = 1;
} else {
res = temp.first->siz + 1;
}
root = merge(temp.first, temp.second);
return res;
}
int query_val_by_rank(Node *root, int rk) {
Node *l, *mid, *r;
tie(l, mid, r) = split_by_rk(root, rk);
// debug
if (mid == nullptr){
return 2147483647;
}
int res = mid->val;
root = merge(merge(l, mid), r);
return res;
}
int query_pre(int val) {
auto temp = split(root, val - 1);
// debug
if (temp.first == nullptr){
return -2147483647;
}
int res = query_val_by_rank(temp.first, temp.first->siz);
root = merge(temp.first, temp.second);
return res;
}
int query_nxt(int val) {
auto temp = split(root, val);
// debug
if (temp.second == nullptr){
return 2147483647;
}
int res = query_val_by_rank(temp.second, 1);
root = merge(temp.first, temp.second);
return res;
}
};
int N;
Nrot_Treap tr;
int main() {
srand(time(0));
scanf("%d", &N);
for (int i = 1; i <= N; i++) {
int op, data;
scanf("%d%d", &op, &data);
if (op == 1) {
tr.insert(data);
} else if (op == 2) {
tr.del(data);
} else if (op == 3) {
printf("%d\n", tr.query_rank_by_val(tr.root, data));
} else if (op == 4) {
printf("%d\n", tr.query_val_by_rank(tr.root, data));
} else if (op == 5) {
printf("%d\n", tr.query_pre(data));
} else if (op == 6) {
printf("%d\n", tr.query_nxt(data));
}
}
return 0;
}
创作不易,各位看官若觉得有用就点个赞吧~