Treaq(FHQ)
数据结构浅析
前置知识
BST(二叉搜索树) 性质:
- 左子节点上的权值比父节点的权值小。
- 右子节点上的权值比父节点的权值大。
既:\(left \lt root \lt right\)。
堆的性质(大根堆):
- 子节点的权值比父亲节点的权值小,既 \(root \geq left, \text{且}root \geq right\)(小根堆恰恰相反)
Treap的定义
Treap:树堆,“树堆”=“树”+“堆”,是一种既满足BST 性质又满足堆性质的一种数据结构。
Treap 的原理
容易发现,若使用同一个值作为两个数据结构的权值,那么两种数据结构结合后会变成一条链,所以平衡树在维护 BST 的权值 \(val\) 的基础上,引进了一个用堆维护的值 \(rd\)。其中 \(rd\) 的值随机给出。
这样维护,通过随机的 \(rd\) 值,打乱了节点插入的顺序,大大降低了因为插入顺序导致 BST 退化为链的可能,便能以 \(O(\log n)\) 的期望复杂度完成一系列操作。
无旋 Treap(FHQ Treap)
无旋 Treap 是一种简洁优美的数据结构,其代码量短较小,却几乎能支持 Treap 的所有功能。
FHQ Treap 有两种核心操作:分裂,合并。诸多复杂操作均基于这两种操作。
对于 FHQ Treap,节点要存的信息有其左儿子 \(ls\),右儿子 \(rs\),BST 权值 \(val\),堆的权值 \(rd\),子树大小 \(sz\)。FHQ Treap 将 \(val\) 相同的点看作多个节点,所以不用记录节点的存储的个数。
核心操作
新建节点
初始化一个有权值 \(val\) 的节点,毕竟平衡树维护的是一个个节点。
点击查看代码
int node; // 节点的编号
int newnode(int v) {
x = ++node;
tr[x].rd = rand(), tr[x].sz = 1, tr[x].val = v;
return x;
}
更新子树大小
分裂或合并后总要更新的吧。
令 \(sz_{root} = sz_{ls} + sz_{rs}\)。
点击查看代码
void push(int p) {tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].sz + 1; }
分裂
有两种分裂方式:按权值 \(val\) 分裂,另一种为按树的大小分裂。
按权值分裂
给定一个值 \(v\),要将原来的平衡树 \(T\) 分为两棵平衡树 \(T_x\) 和 \(T_y\),\(\forall i \in T_x, j \in T_y, val_i \leq v \lt val_j\)。
假设当前分裂到一个节点 \(p\),如果 \(val_p \leq v\),那么 \(p\) 应该属于 \(T_x\),且显然 \(p\) 的左子树均应该属于 \(T_x\),故只需向右递归寻找 \(p\) 的右子树有那些属于 \(T_x\),那些应该属于 \(T_y\) 即可。反之,\(p\) 应该属于 \(T_y\),故需要在 \(p\) 的左子树中寻找那些属于 \(T_x\),那些属于 \(T_y\)。
如何拼接子树到平衡树上:让该子树的最大的父亲的父亲节点指向它即可。
点击查看代码
// P:当前分裂到的节点,v:分裂的值,x,y:将该节点子树分裂后的两棵平衡树
void split(int p, int v, int &x, int &y) {
if (!p) { x = y = 0; return;} // 该节点不存在则返回
if (tr[p].val <= v) split(tr[p].rs, v, tr[x = p].rs, y);
else split(tr[p].ls, v, x, tr[y = p].ls);
push(p);
}
按树的大小分裂
给定一个值 \(v\),要将原来的平衡树 \(T\) 分为两棵平衡树 \(T_x\) 和 \(T_y\),\(sz_x=v,sz_y=n-v\)。
与按权值分裂大致相同。
假设当前分裂到一个节点 \(p\),如果 \(sz_{p_{ls}} + 1 \leq v\),那么 \(p\) 应该属于 \(T_x\),且显然 \(p\) 的左子树均应该属于 \(T_x\),故只需向右递归寻找 \(p\) 的右子树有那些属于 \(T_x\),那些应该属于 \(T_y\) 即可。反之,\(p\) 应该属于 \(T_y\),故需要在 \(p\) 的左子树中寻找那些属于 \(T_x\),那些属于 \(T_y\)。
点击查看代码
// P:当前分裂到的节点,v:分裂的值,x,y:将该节点子树分裂后的两棵平衡树
void split(int p, int v, int &x, int &y) {
if (!p) { x = y = 0; return;} // 该节点不存在则返回
if (tr[tr[p].ls] + 1 <= v) split(tr[p].rs, v, tr[x = p].rs, y);
else split(tr[p].ls, v, x, tr[y = p].ls);
push(p);
}
合并
能将两棵平衡树合并,如平衡树 \(T_x, T_y\),应该有 \(\forall i \in T_x, j \in T_y,i \le j\),这是由我们满足的,如果操作不满足这个性质,那么平衡树就不满足 BST 性质。
合并的过程:将一棵平衡树接到另外一棵平衡树上。
因操作满足 \(\forall i \in T_x, j \in T_y,i \le j\),所以仅需维护堆即可。
具体来说,若 \(x\) 的 \(rd\) 大于 \(y\) 的 \(rd\),则合并 \(x_{ls}\) 和 \(y\),否则相反。
点击查看代码
int merge(int x, int y) {
if (!x || !y) return x | y; // 如果合并过程中有一个树没有儿子,那么把有儿子的拼接上就行
if (tr[x].rd > tr[y].rd) { // 维护堆性质
tr[x].rs = merge(tr[x].rs, y), push(x);
return x;
}
else {
tr[y].ls = merge(x, tr[y].ls), push(y);
return y;
}
}
例题
luogu P3369【模板】普通平衡树
平衡树模板题,需要支持 6 种操作:
- 插入一个数
- 删除一个数
- 查询一个数的排名(排名为比当前数小的数的个数 \(+1\))
- 查询排名为 \(x\) 的数
- 求 \(x\) 的前驱(小于 \(x\) 且最大的数)
- 求 \(x\) 的后驱(大于 \(x\) 且最小的数)
插入
设插入一个数 \(x\),将平衡树 \(T\) 按 \(x - 1\) 分裂成两棵平衡树 \(T_x\) 和 \(T_y\),\(\forall i \in T_x, j \in T_y, i \lt x, x \lt j\)。
新建一个 \(val\) 为 \(c\) 的节点,先将 \(T_x\) 与 \(c\) 节点合并得新的 \(T_x\),再将 \(T_x\) 和 \(T_y\) 合并即可。
点击查看代码
void insert(int v) {
int x = 0, y = 0;
split(R, v - 1, x, y);// R 是根节点
R = merge(merge(x, newnode(v)), y);
}
删除
设删除一个数 \(x\),将平衡树 \(T\) 按 \(x - 1,x\) 分裂出三棵平衡树 \(T_x,T_y,T_z\)。
然后合并 \(T_y\) 的左右儿子(没有左右儿子合并是 \(0\),没有影响)得到新的 \(T_y\)(这样可以认为把 \(T_y\) 的根去掉了)然后把三棵平衡树合并起来即可。
点击查看代码
void _delete(int v) {
int x = 0, y = 0, z = 0;// R 是根节点
split(R, v - 1, x, y), split(y, v, y, z);
R = merge(merge(x, merge()))
}
查询排名
统计比 \(v\) 大的数有多少即可,将平衡树 \(T\) 分裂为两棵平衡树 \(T_x,T_y\),其中 \(\forall i \in T_x,j\in T_y,i \lt v \leq j\)。答案就是 \(sz_x\),注意分裂后要合并还原。
点击查看代码
int rank(int v) {
int x = 0, y = 0,ans = 0;
split(R, x - 1, x, y);
ans = tr[v].sz;
R = merge(x, y);
return ans;
}
查询排名为 \(k\) 的值
因为平衡树满足 BST 性质,所以查询第 \(k\) 小的流程很简单。
具体来说:若当前查询到一点 \(p\),如果它的左儿子的 \(sz\) 比 \(k\) 大,那么答案一定在左子树里。而若它的左子树的 \(sz\) 加上 \(1\) 等于 \(k\),说明 \(p\) 就是第 \(k\) 小。否则,答案一定在右子树里,向右递归即可。
点击查看代码
int kth(int k) {
int p = R;
while (1) {
if (tr[tr[p].ls].sz <= k) p = tr[p].ls;
else if (tr[tr[p]].ls + 1 == k) return tr[p].val;
else k -= tr[p].sz + 1, p = tr[p].rs;
}
}
查询前驱
与查询第 \(k\) 大很像。
因为要找最大的比 \(x\) 小的值,所以查询过程中,若当前点比 \(x\) 小,更新答案,递归到右子树即可,否则,递归左子树。
点击查看代码
int pre(int v) {
int p = R, ans = 0;
while (1) {
if (!p) return ans;
else if (v <= tr[p].val) p = tr[p].ls;
else ans = tr[p].val,p = tr[p].rs;
}
}
查询后驱
与查询前驱反过来即可。
点击查看代码
int suc(int v) {
int p = R, ans = 0;
while (1) {
if (!p) return ans;
else if (v >= tr[p].val) p = tr[p].rs;
else ans = tr[p].val, p = tr[p].ls;
}
}
完整代码
点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
void RD() {}
template<typename T, typename... U> void RD(T &x, U&... arg) {
x = 0; int f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
x *= f; RD(arg...);
}
const int N = 1e5 + 5;
struct tree { int val, rd, sz, ls, rs; } tr[N];
int n, node, R;
int newnode(int v) {
int x = ++node;
tr[x].val = v, tr[x].rd = rand(), tr[x].sz = 1;
return x;
}
void push(int p) { tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].sz + 1; }
void split(int p, int v, int &x, int &y) {
if (!p) { x = y = 0; return; }
if (tr[p].val <= v) split(tr[p].rs, v, tr[x = p].rs, y);
else split(tr[p].ls, v, x, tr[y = p].ls);
push(p);
}
int merge(int x, int y) {
if (!x || !y) return x | y;
if (tr[x].rd > tr[y].rd) {
tr[x].rs = merge(tr[x].rs, y);
push(x); return x;
}
else {
tr[y].ls = merge(x, tr[y].ls);
push(y); return y;
}
}
void insert(int v) {
int x = 0, y = 0;
split(R, v - 1, x, y);
R = merge(merge(x, newnode(v)), y);
}
void _delete(int v) {
int x = 0, y = 0, z = 0;
split(R, v - 1, x, z), split(z, v, y, z);
R = merge(merge(x, y = merge(tr[y].ls, tr[y].rs)), z);
}
int rand(int v) {
int x = 0, y = 0, ans = 0;
split(R, v - 1, x, y);
ans = tr[x].sz + 1;
R = merge(x, y);
return ans;
}
int kth(int k) {
int p = R;
while (1) {
if (k <= tr[tr[p].ls].sz) p = tr[p].ls;
else if (k == tr[tr[p].ls].sz + 1) return tr[p].val;
else k -= tr[tr[p].ls].sz + 1, p = tr[p].rs;
}
}
int pre(int v) {
int p = R, ans = 0;
while (1) {
if (!p) return ans;
else if (v <= tr[p].val) p = tr[p].ls;
else ans = tr[p].val,p = tr[p].rs;
}
}
int suc(int v) {
int p = R, ans = 0;
while (1) {
if (!p) return ans;
else if (v >= tr[p].val) p = tr[p].rs;
else ans = tr[p].val, p = tr[p].ls;
}
}
int main() {
RD(n);
while (n--) {
int opt, x;
RD(opt, x);
if (opt == 1) insert(x);
if (opt == 2) _delete(x);
if (opt == 3) printf("%d\n", rand(x));
if (opt == 4) printf("%d\n", kth(x));
if (opt == 5) printf("%d\n", pre(x));
if (opt == 6) printf("%d\n", suc(x));
}
return 0;
}
我们能够爱的人,我们也能恨他们。而其余的人,则对我们无关紧要。