「笔记」FHQ-Treap
前置知识 :二叉搜索树
简介
一种二叉搜索树的实现方法。
treap = tree + heap,是一种树和堆组合形成的数据结构。
treap 的每个节点上额外储存一个 关键值。
treap 除满足二叉搜索树性质外,还满足父节点的关键值 \(\ge\) 儿子的关键值。
若关键值随机生成,则 treap 树高期望为\(O(\log n)\)。
treap 分为旋转式和无旋式(FHQ-treap)两种,码量均较小。
无旋 treap 以分裂和合并为核心。
其操作方式使它天生支持维护序列、可持久化等特性。
核心操作
分裂
\(\text{Split}(now, Val, \&x, \&y)\) 将以\(now\)节点为根的子树按 参数 \(Val\) 分裂为两颗子树 \(x\), \(y\),并返回其编号。
分为 按权值分裂 和 按关键值分裂 两种。
给定一棵子树,按照给定参数 分裂为两棵子树。
一棵节点的参数都小于等于给定参数,另一棵大于给定参数。
以下以按权值分裂为例,称权值较小的树为左树,另一棵为右树。
根据二叉搜索树左小右大的性质,可确定分裂的过程:
- 比较根节点权值 与 给定权值的大小关系
- 若小于给定权值,则根及左子树点都小于等于给定权值,将其归入左树中。
向右子树递归,继续分裂子树。 - 否则,根及右子树点都大于等于给定权值,将其归入右树中。
向左子树递归,继续分裂子树。
- 若小于给定权值,则根及左子树点都小于等于给定权值,将其归入左树中。
- 递归至叶节点后退出。
显然单次分裂 复杂度是 \(O(\text{Height})\) 的。
期望树高为\(\log n\),单次分裂期望复杂度为\(O(\log n)\)。
void Split(int now, int Val, int &x, int &y)
{
if(! now) {x = y = 0; return ;}
if(t[now].val <= Val) x = now, Split(t[now].son[1], Val, t[now].son[1], y);
else y = now, Split(t[now].son[0], Val, x, t[now].son[0]);
Update(now);
}
合并
\(\text{Merge}(x, y)\) 将以\(x\),\(y\)节点为根的两棵树合并为一棵,并返回新树根的编号。
特别注意:合并的两棵树 一定是 Split 分裂获得的两棵树
即 Merge 操作的出现, 一定是下列形式的:
Split(Root, Val, tmp1, tmp2);
Root = Merge(tmp1, tmp2);
经过 Split 后,得到的两棵 treap 是有序的,左树任一点权值 必然小于右树任一点。
只需要考虑按关键值,确定父子关系即可。
- 若左树根的关键值小于右树根关键值,则将左树根作为新树根。
新树继承 左树的左子树。
递归,将 右树 与 左树的右子树 合并,作为新树的右子树。 - 否则,将右树根作为新树根,新树继承 右树的右子树。
递归,将 左树 与 右树的左子树 合并,作为新树的左子树。 - 当左 / 右子树不存在时退出。
int Merge(int x, int y)
{
if(! x || ! y) return x + y;
if(t[x].Rand < t[y].Rand)
{
t[x].son[1] = Merge(t[x].son[1], y); Update(x);
return x;
}
t[y].son[0] = Merge(x, t[y].son[0]); Update(y);
return y;
}
操作
基本操作
\(\text{Update}(x)\) 更新以\(x\)为根的子树大小。
\(\text{NewNode}(Val)\) 新建一权值为 \(Val\) 的节点,并返回其编号。
void Update(int x) {t[x].size = t[t[x].son[0]].size + t[t[x].son[1]].size + 1;}
int NewNode(int Val)
{
NodeNum ++;
t[NodeNum].size = 1; t[NodeNum].val = Val; t[NodeNum].Rand = rand();
return NodeNum;
}
插入操作
\(\text{Insert}(Val)\) 新建一个权值为 \(Val\) 的节点,并将其插入至treap中。
先按照 \(Val\) 进行分裂,得到两棵满足上述条件的treap。
新建一权值为 \(Val\) 的节点,先与左树合并(左树点权值 \(< Val\)),再与右树合并 (右树点权值 \(\ge Val\))。
void Insert(int Val)
{
Split(Root, Val, tmp1, tmp2);
Root = Merge(Merge(tmp1, NewNode(Val)), tmp2);
}
删除操作
\(\text{Delete}(Val)\) 删除treap中一个权值为 \(Val\)的节点。
按照代码中方式,先将权值为 \(Val\) 的节点分裂至 tmp2 中。
将 tmp2 根的左右子树合并,即删去权值为 \(Val\) 的根节点,保证了只删除一个节点。
再将 tmp1, tmp2, tmp3 合并即可。
void Delete(int Val)
{
Split(Root, Val, tmp1, tmp3), Split(tmp1, Val - 1, tmp1, tmp2);
tmp2 = Merge(t[tmp2].son[0], t[tmp2].son[1]);
Root = Merge(Merge(tmp1, tmp2), tmp3);
}
查询给定权值的排名
\(\text{QueryRank}(Val)\) 查询权值 \(Val\) 的排名。
按权值\(Val - 1\)分裂,所得左树的大小 + 1即为排名。
void QueryRank(int Val)
{
Split(Root, Val - 1, tmp1, tmp2);
printf("%d\n", t[tmp1].size + 1);
Root = Merge(tmp1, tmp2);
}
查询给定排名的权值
\(\text{Kth}(now, Rank)\) 查询排名 \(Rank\) 在子树 \(now\) 中的权值,返回对应节点的编号。
根据二叉树搜索树左小右大的性质 和 维护的子树大小可得答案。
- 若左子树非空 且 剩余排名 \(Rank\) 不大于左子树的 Size,则向左子树查找。
- 若 $Rank = $ 左子树大小 +1,说明当前节点即为所求,返回其编号。
- 否则将 \(Rank\) 减去左子树大小 +1,向右子树查找。
int Kth(int now, int Key)
{
while(1)
{
if(Key <= t[t[now].son[0]].size) {now = t[now].son[0]; continue;}
if(Key == t[t[now].son[0]].size + 1) return now;
Key -= t[t[now].son[0]].size + 1;
now = t[now].son[1];
}
}
查询前驱后继
\(\text{QueryPrecursor}(Val)\) 查询 \(Val\) 的前驱。
前驱定义为小于 \(Val\) 的最大的数。
按权值\(Val - 1\)分裂,所得左树中节点权值均小于\(Val\)。
左树中最大的值,即排名为左树Size的值,即为前驱。
void QueryPrecursor(int Val)
{
Split(Root, Val - 1, tmp1, tmp2);
printf("%d\n", t[Kth(tmp1, t[tmp1].size)].val);
Root = Merge(tmp1, tmp2);
}
\(\text{QuerySuccessor}(Val)\) 查询 \(Val\) 的后继。
后继定义为大于 \(Val\) 的最小的数。
按权值\(Val\)分裂,所得右树中节点权值均大于\(Val\)。
右树中最小的值,即排名为1的值,即为后继。
void QuerySuccessor(int Val)
{
Split(Root, Val, tmp1, tmp2);
printf("%d\n", t[Kth(tmp2, 1)].val);
Root = Merge(tmp1, tmp2);
}
完整代码
//fhq-treap
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstdlib>
#include <algorithm>
#define min std::min
#define max std::max
#define ll long long
const int MARX = 1e5 + 10;
//=============================================================
struct FhqTreapNode
{
int son[2], val, size, Rand;
} t[MARX];
int N, NodeNum, Root;
int tmp1, tmp2, tmp3;
//=============================================================
inline int read()
{
int f = 1, w = 0; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Update(int x) {t[x].size = t[t[x].son[0]].size + t[t[x].son[1]].size + 1;}
int NewNode(int Val)
{
NodeNum ++;
t[NodeNum].size = 1; t[NodeNum].val = Val; t[NodeNum].Rand = rand();
return NodeNum;
}
int Merge(int x, int y)
{
if(! x || ! y) return x + y;
if(t[x].Rand < t[y].Rand)
{
t[x].son[1] = Merge(t[x].son[1], y); Update(x);
return x;
}
t[y].son[0] = Merge(x, t[y].son[0]); Update(y);
return y;
}
void Split(int now, int Val, int &x, int &y)
{
if(! now) {x = y = 0; return ;}
if(t[now].val <= Val) x = now, Split(t[now].son[1], Val, t[now].son[1], y);
else y = now, Split(t[now].son[0], Val, x, t[now].son[0]);
Update(now);
}
int Kth(int now, int Rank)
{
while(1)
{
if(Rank <= t[t[now].son[0]].size) {now = t[now].son[0]; continue;}
if(Rank == t[t[now].son[0]].size + 1) return now;
Rank -= t[t[now].son[0]].size + 1, now = t[now].son[1];
}
}
void Insert(int Val)
{
Split(Root, Val, tmp1, tmp2);
Root = Merge(Merge(tmp1, NewNode(Val)), tmp2);
}
void Delete(int Val)
{
Split(Root, Val, tmp1, tmp3), Split(tmp1, Val - 1, tmp1, tmp2);
tmp2 = Merge(t[tmp2].son[0], t[tmp2].son[1]);
Root = Merge(Merge(tmp1, tmp2), tmp3);
}
void QueryRank(int Val)
{
Split(Root, Val - 1, tmp1, tmp2);
printf("%d\n", t[tmp1].size + 1);
Root = Merge(tmp1, tmp2);
}
void QueryPrecursor(int Val)
{
Split(Root, Val - 1, tmp1, tmp2);
printf("%d\n", t[Kth(tmp1, t[tmp1].size)].val);
Root = Merge(tmp1, tmp2);
}
void QuerySuccessor(int Val)
{
Split(Root, Val, tmp1, tmp2);
printf("%d\n", t[Kth(tmp2, 1)].val);
Root = Merge(tmp1, tmp2);
}
//=============================================================
int main()
{
srand(114514);
N = read();
for(int i = 1; i <= N; ++ i)
{
int opt = read(), x = read();
if(opt == 1) Insert(x);
if(opt == 2) Delete(x);
if(opt == 3) QueryRank(x);
if(opt == 4) printf("%d\n", t[Kth(Root, x)].val);
if(opt == 5) QueryPrecursor(x);
if(opt == 6) QuerySuccessor(x);
}
return 0;
}
写在最后
参考资料
\(\text{Oi-Wiki-Treap}\)
题解 P3369 【【模板】普通平衡树】 - ARFA - 洛谷博客 (图解超详细建议阅读)