\(FHQ\) \(Treap\)
视频讲解
优质讲解
一、普通\(Treap\)
\(Treap\)= \(Tree\) + \(Heap\)
在平衡树的每个节点存放两个信息:
值
值满足二叉搜索树(\(BST\))的性质
随机修复值\(fix\)
修复值满足堆的性质
二叉搜索树的性质
当前结点左子树的值都比当前结点值小,反之则一定在右子树上。
二叉堆的性质(大根堆)
父结点的优先级总是大于或等于任何一个子节点的优先级。
这里提到的优先级,就是上面提到的索引。
总结
在保留二叉搜索树的中序遍历不变的前提下,采用一定的策略(二叉堆+随机数),使树尽量的均衡,就是平衡树的本质,中庸之道也~
二、\(Treap\)为什么可以平衡?
我们发现,\(BST\)会遇到不平衡的原因是因为有序的数据会使查找的路径退化成链
而随机的数据使\(BST\)退化的概率是非常小的
在\(Treap\)中,修正值的引入恰恰是使树的结构不仅仅取决于节点的值,还取决于修正值的值
然而修正值的值是随机生成的,出现有序的随机序列是小概率事件,
所以\(Treap\)的结构是趋向于随机平衡的。
三、\(FHQ\) \(Treap\)
优点:码量小而好写,核心操作的代码都是复读机
好理解、支持的操作多
缺点:常数略大(很大~)
平衡树双子星,很牛B的样子。
四、奇怪的操作
普通\(Treap\)用来维护树平衡的 奇怪的操作是什么呢?
树旋转
\(FHQ\) \(Treap\)的奇怪操作并且是核心操作只有两个:
\(split\) , \(merge\)
只需掌握好这两个基本操作,那么你基本上就已经掌握了\(FHQ\) \(Treap\)了。
五、\(FHQ\)存储的信息
结点信息(\(5\)个):
左右子树编号 这个所有平衡树都一样
值 \(val\)
修复值(随机) \(fix\)
子树大小 \(size\)
六、分裂(\(split\))
分裂有两种:按值分裂和按大小分裂
按值分裂:把树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另外一部分全都大于给定的值。
按大小分裂:把树拆成两棵树,拆出来的一棵树的值全部小于给定的大小,另外一部分的值全部大于等于给定的大小。
一般当平衡树来用的时候,用的是按值分裂。 在维护区间信息的时候,采用按大小分裂。很经典的例子就是文艺平衡树,这个稍后再说。
七、合并(\(merge\))
八、代码模板
P3369 【模板】普通平衡树
AcWing 253 普通平衡树
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int INF = 0x3f3f3f3f;
mt19937 rnd(233); //高性能随机数生成器 随机范围大概在(maxint,+maxint),233为种子,19937指该随机数循环节为2^19937
//重定义ls,rs
#define ls tr[p].l
#define rs tr[p].r
struct Node {
int l, r; //左右儿子的节点号
int rnd; //在堆中编号,随机值
int size; //以当前节点为根的子树中节点的个数
int val; //值
//这里没有记录当前节点值的个数,是不是有点奇怪?
} tr[N];
int root, idx;
//向父节点更新统计信息
void pushup(int p) {
tr[p].size = tr[ls].size + tr[rs].size + 1;
}
//创建一个新节点
int newnode(int val) {
tr[++idx].rnd = rand();
tr[idx].size = 1;
tr[idx].val = val;
return idx;
}
//将以p为根的平衡树进行分裂,小于等于x的都放到以pl为根的子树中,大于x都放到以pr为根的子树中
//因为生成的两个子树最终需要返回两个根,所以这里用了引用的方式
void split(int p, int val, int &x, int &y) {
if (!p) { //当前节点为空,还分裂个屁~
x = y = 0;
return;
}
if (tr[p].val <= val) //当前点归左边,如果当前点<=x,那么它的左子必然也<=x
x = p, split(rs, val, rs, y); //把当前点p接在pl上,继续分它的右子,它的右子可能还有>x的
else
y = p, split(ls, val, x, ls); //把当前点p接在pr上,继续分它的左子,它的左子可能还有<=x的
//推送统计信息
pushup(p);
}
//将以pl,pr为根的两个子树合并成一棵树
//要求:两个子树的值域不能重叠,没有交叉
int merge(int x, int y) {
if (!x || !y) return x + y; //如果pl或者pr有一个是空了,那么返回另一个即可,此处比较取巧,采用了+
int p; //根
if (tr[x].rnd < tr[y].rnd) { //两个都不空,谁来当根呢?需要使用小根堆性质,rk小的当根
p = x; // x当根
tr[x].r = merge(tr[x].r, y); //将y接到x的右子上
} else {
p = y; // y当根
tr[y].l = merge(x, tr[y].l); //将x接到y的左子上
}
//更新统计信息
pushup(p);
return p;
}
//插入操作,插入一个数字val
void insert(int val) {
int x, y, z;
split(root, val, x, z); // 1、小于等于val的划到x子树中,大于val的划到y子树中
y = newnode(val); // 2、创建一个新节点y
root = merge(x, merge(y, z)); // 3、合并三者,因为这三者是需要满足BST有序的,所以顺序不能乱,更新根节点
}
//移除值val
void remove(int val) {
int x, y, z;
split(root, val, x, z); // 1、小于等于v的放x里,大于v的放z里
split(x, val - 1, x, y); // 2、继续分裂x,小于等于v-1的放x里,大于v-1的放y里,即y里全部都是等于val的!
y = merge(tr[y].l, tr[y].r); // 3、y的左子树和右子树合并,相当于把根节点不要了
root = merge(x, merge(y, z)); // 4、合并三者,更新根节点
}
//查询某个值的排名
int get_rank_by_key(int val) {
int x, y;
split(root, val - 1, x, y); // 1、按val-1分裂,此时x中就是所有<=val-1的数
int res = tr[x].size + 1; // 2、统计x为根的树中数字个数+1就是最终排名
root = merge(x, y); // 3、还原,这也太暴力了吧~
return res; // 4、返回排名
}
//查询排名的值
int get_key_by_rank(int k) {
int p = root;
while (p) { // 1、如果当前节点不为空
if (tr[ls].size + 1 == k) // 2、说明当前节点就是要查找的第k个数
return tr[p].val;
if (tr[ls].size >= k) // 3、如果左子树的数字数量大于等于k,在左子树中查找
p = tr[p].l;
else
k -= tr[ls].size + 1, p = rs; // 4、换算下在右子树中查找
}
return -1; // 5、没有找到返回-1
}
//寻找v的前驱
int get_prev(int val) {
int x, y;
split(root, val - 1, x, y); // 1、按v-1分裂,x里的最大的数就是val的前驱
int p = x; // 2、目标肯定在左子中
while (rs) p = rs; // 3、左子的最右边就是答案
int res = tr[p].val; // 4、记录答案
root = merge(x, y); // 5、还原回去
return res;
}
//寻找v的后继
int get_next(int val) {
int x, y;
split(root, val, x, y); // 1、按v分裂,y里的最小的就是val的后继
int p = y; // 2、答案肯定在右子中
while (ls) p = ls; // 3、右子的最左边就是答案
int res = tr[p].val; // 4、记录答案
root = merge(x, y); // 5、还原回去
return res;
}
int main() {
// fhq可以有重复的点
//为了防止找前驱后继时要找的数比FHQ中的数都小/大(比如我们要找FHQ最小的数的前驱)
//我们可以一开始就加入−∞,∞两个哨兵节点。
insert(-INF), insert(INF);
int q;
scanf("%d", &q);
while (q--) {
int op, x;
scanf("%d%d", &op, &x);
if (op == 1)
insert(x);
else if (op == 2)
remove(x);
else if (op == 3)
printf("%d\n", get_rank_by_key(x) - 1);
else if (op == 4)
printf("%d\n", get_key_by_rank(x + 1));
else if (op == 5)
printf("%d\n", get_prev(x));
else if (op == 6)
printf("%d\n", get_next(x));
}
return 0;
}