平衡树之FHQ-treap
前置知识:二叉查找树
首先我们要看一下二叉查找树
它满足这些性质:
1.它是二叉树(废话)
2.对于任何一个根节点 它左子树的所有点都小于它 它右子树的所有点都大于它
所以实际上它的中序遍历就是对整个序列排序的结果
它非常的方便 支持查询很多的东西(后面会讲)
但是如果只是普通的插入 很容易让它复杂度假掉 所以我们需要一些手段来维持它的平衡
这就是平衡树
FHQ-treap
首先我们来看一下什么是 \(treap\)
实际上\(treap = tree + heap\) 非常的震撼啊我也没想到
具体是怎么回事呢
因为大根堆是一颗完全二叉树
我们给 \(BST\) 的每个节点都随机一个权值 然后拿这个权值把他们按大根堆排起来
那么这棵树就是期望平衡的
其中新建点和维护的操作如下:
inline void maintain(int k) {
T[k].siz = T[T[k].l].siz + T[T[k].r].siz + 1;
}
int newnode(int val) {
T[++tot].val = val;
T[tot].siz = 1;
T[tot].dat = rand();
return tot;
}
而 \(FHQ-treap\) 没有 \(treap\) 的旋转操作 它非常的好学
它基于两个操作:分裂(split)和合并(merge)
- split
我们把根为 \(k\) 的树按 \(key\) 分成 \(x y\) 两个部分
并且使 \(x\) 中的都小于等于 \(key\) \(y\) 中的都大于 \(key\)
代码如下:
void split(int k, int key, int &x, int &y) { //分裂的时候保证x中所有数都小于y中所有数
if (k == 0) {
x = y = 0;
return;
}
if (T[k].val <= key) { //键值大于val 就把这个点和它的左子树全给x
x = k;
split(T[k].r, key, T[x].r, y); // 然后把它的右节点分成两半 一半给x的右子树一半给y
} else { // 反之亦然
y = k;
split(T[k].l, key, x, T[y].l);
}
maintain(k);
}
- merge
我们把 \(x y\) 两棵树合起来 并返回新的根节点
代码如下:
int merge(int x, int y) { //顺序不能反 理由同上
if (x == 0 || y == 0) return x + y;
if (T[x].dat > T[y].dat) { //维护treap的大根堆性质
T[x].r = merge(T[x].r, y); //x的根节点应该在上面 所以把y合到x的右子树
maintain(x);
return x;
}
else { //反之亦然
T[y].l = merge(x, T[y].l);
maintain(y);
return y;
}
}
有了这两个操作 我们就可以干很多的事情:
- insert
void insert(int key) { //插入键值为key的数
int x, y;
split(root, key - 1, x, y); //把小于等于key-1的分给x 大于等于key的分给y
int id = newnode(key);
root = merge(merge(x, id), y); //把新节点插入到x,y之间
}
- remove
void remove(int key) { //删除一个键值为key的数
int x, y, z;
split(root, key - 1, x, y); //把小于等于key-1的分给x
split(y, key, y, z); //把大于key的分给z 那y中就全是等于key的了
if (y) y = merge(T[y].l, T[y].r); //因为只需要删除一个数 所以把根节点删掉左右俩儿子合并
root = merge(merge(x, y), z); //然后重新合起来 如果要把权值为key的全删掉就把xz合起来
}
- rank
int rank(int key) { //查询key的排名
int x, y, ans;
split(root, key - 1, x, y); //把小于key的数都分给x 那么siz[x]就是小于key的数的数量
ans = T[x].siz + 1;
root = merge(x, y); //掰完记得合起来
return ans;
}
- kth
int kth(int rnk) { //查询排名为rnk的数
int p = root;
while (p) {
if (T[T[p].l].siz + 1 == rnk) break; //正正好好查到
else if (T[T[p].l].siz + 1 > rnk) p = T[p].l; //查过头了
else {
rnk -= T[T[p].l].siz + 1;
p = T[p].r; //还不够
}
}
return T[p].val;
}
- get_pre
int get_pre(int key) { //查询key的前驱
int x, y, p;
split(root, key - 1, x, y); //把比key小的都分给x
p = x;
while (T[p].r != 0) p = T[p].r; //一直往大的方向走
root = merge(x, y);
return T[p].val;
}
- get_nxt
int get_nxt(int key) { //查询key的后缀
int x, y, p;
split(root, key, x, y); //把比key大的都分给y
p = y;
while (T[p].l != 0) p = T[p].l;
root = merge(x, y);
return T[p].val;
}
最后附上P3369 【模板】普通平衡树的代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 0721;
int n;
struct tree {
struct node {
int l, r; //左儿子 右儿子
int val, siz, dat; //权值 大小 堆中的权值
} T[N];
int tot = 0, root = 0;
inline void maintain(int k) {
T[k].siz = T[T[k].l].siz + T[T[k].r].siz + 1;
}
int newnode(int val) {
T[++tot].val = val;
T[tot].siz = 1;
T[tot].dat = rand();
return tot;
}
void split(int k, int key, int &x, int &y) { //分裂的时候保证x中所有数都小于y中所有数
if (k == 0) {
x = y = 0;
return;
}
if (T[k].val <= key) { //键值大于val 就把这个点和它的左子树全给x
x = k;
split(T[k].r, key, T[x].r, y); // 然后把它的右节点分成两半 一半给x的右子树一半给y
} else { // 反之亦然
y = k;
split(T[k].l, key, x, T[y].l);
}
maintain(k);
}
int merge(int x, int y) { //顺序不能反 理由同上
if (x == 0 || y == 0) return x + y;
if (T[x].dat > T[y].dat) { //维护treap的大根堆性质
T[x].r = merge(T[x].r, y); //x的根节点应该在上面 所以把y合到x的右子树
maintain(x);
return x;
}
else { //反之亦然
T[y].l = merge(x, T[y].l);
maintain(y);
return y;
}
}
void insert(int key) { //插入键值为key的数
int x, y;
split(root, key - 1, x, y); //把小于等于key-1的分给x 大于等于key的分给y
int id = newnode(key);
root = merge(merge(x, id), y); //把新节点插入到x,y之间
}
void remove(int key) { //删除一个键值为key的数
int x, y, z;
split(root, key - 1, x, y); //把小于等于key-1的分给x
split(y, key, y, z); //把大于key的分给z 那y中就全是等于key的了
if (y) y = merge(T[y].l, T[y].r); //因为只需要删除一个数 所以把根节点删掉左右俩儿子合并
root = merge(merge(x, y), z); //然后重新合起来 如果要把权值为key的全删掉就把xz合起来
}
int rank(int key) { //查询key的排名
int x, y, ans;
split(root, key - 1, x, y); //把小于key的数都分给x 那么siz[x]就是小于key的数的数量
ans = T[x].siz + 1;
root = merge(x, y); //掰完记得合起来
return ans;
}
int kth(int rnk) { //查询排名为rnk的数
int p = root;
while (p) {
if (T[T[p].l].siz + 1 == rnk) break; //正正好好查到
else if (T[T[p].l].siz + 1 > rnk) p = T[p].l; //查过头了
else {
rnk -= T[T[p].l].siz + 1;
p = T[p].r; //还不够
}
}
return T[p].val;
}
int get_pre(int key) { //查询key的前驱
int x, y, p;
split(root, key - 1, x, y); //把比key小的都分给x
p = x;
while (T[p].r != 0) p = T[p].r; //一直往大的方向走
root = merge(x, y);
return T[p].val;
}
int get_nxt(int key) { //查询key的后缀
int x, y, p;
split(root, key, x, y); //把比key大的都分给y
p = y;
while (T[p].l != 0) p = T[p].l;
root = merge(x, y);
return T[p].val;
}
} treap;
int main() {
srand(time(0));
scanf("%d", &n);
while (n--) {
int opt, x;
scanf("%d%d", &opt, &x);
if (opt == 1) treap.insert(x);
else if (opt == 2) treap.remove(x);
else if (opt == 3) printf("%d\n",treap.rank(x));
else if (opt == 4) printf("%d\n",treap.kth(x));
else if (opt == 5) printf("%d\n",treap.get_pre(x));
else printf("%d\n",treap.get_nxt(x));
}
return 0;
}
特别鸣谢:@james1BadCreeper 二叉搜索树与平衡树