浅析FHQ-treap
fhq-treap 又名“无旋 treap”,有着码量小,易理解,可持久化等特点。
前言
fhq-treap 又名“无旋 treap”,有着码量小,易理解,可持久化等特点。
但 fhq-treap 的常数较大。
必要:
- 二叉搜索树
- 堆
选要:
基础操作
treap 的每个节点都有一个随机的优先级。
treap 的权值要具有二叉搜索树的性质,优先级满足堆的性质。
fhq-treap 有两个关键函数 slipt
和 merge
,分别是分裂和合并。
代码中均为小根堆。
节点
在 fhq-treap 中我们需要维护子树大小,节点权值,左右儿子,优先级(随机数,用于堆)。
std::mt19937 rd(std::chrono::steady_clock::now().time_since_epoch().count());
//需要 <random> 和 <chrono> 头文件,不要放在结构体内
struct node{
int size, val, rank, ls, rs;
}d[N];
int tot = 0, root = 0;
int newnode(int val){
d[++tot].val = val, d[tot].rank = rd();
d[tot].size = 1, d[tot].ls = d[tot].rs = 0;
return tot;
}
void getsize(int u){d[u].size = 1 + d[d[u].ls].size + d[d[u].rs].size;}
分裂
按权值分裂
明显,我们会把一个树,把权值
void SplitVal(int now, int val, int&L, int&R){
if(!now)return void(L = R = 0);
if(d[now].val <= val)L = now, SplitVal(d[now].rs, val, d[now].rs, R);
else R = now, SplitVal(d[now].ls, val, L, d[now].ls);
return getsize(now);
}
我们理解一下代码。
我们现在遍历到了原树的 now 节点。
如果节点是空的那么 L 和 R 树都是空的。
如果这个节点的权值
反之同理。
按排名分裂
同理,只不过要判断左儿子的大小关系。
void SplitRank(int now, int k, int&L, int&R){
if(!now)return void(L = R = 0);
int size = d[d[now].ls].size;
if(k <= size)R = now, SplitRank(d[now].ls, k, L, d[R].ls);
else L = now, SplitRank(d[L].rs, k - size - 1, d[L].rs, R);
getsize(now);
}
合并
合并是分裂的逆操作,我们回到之前的这幅图。
如果要合并,我们需要保证
由于两棵树有序,只需要根据优先级考虑哪颗树放“上面”,哪棵树放“下面”,即考虑哪棵树成为子树。
同时,我们还需要满足二叉搜索树的性质。
所以若
反之,则
int merge(int L, int R){
if(!L || !R)return L + R;
if(d[L].rank < d[R].rank){
d[L].rs = merge(d[L].rs, R), getsize(L);
return L;
}
else {
d[R].ls = merge(L, d[R].ls), getsize(R);
return R;
}
}
其他
有了分裂和合并,剩下的函数就很好实现了。
插入
新建的节点的权值的
void insert(int val){
int L, R;
SplitVal(root, val, L, R);
root = merge(merge(L, newnode(val)), R);
}
这样我们需要调用一次分裂和两次合并,常数明显很大。
我们可以优化:
void insert(int val){
int*u = &root, z = newnode(val), r = d[z].rank;
for(;*u && (d[*u].rank < r);u = &(val < d[*u].val ? d[*u].ls : d[*u].rs))
++d[*u].size;
SplitVal(*u, val, d[z].ls, d[z].rs), *u = z, getsize(z);
}
我们可以用循环寻找我们要插入的位置,然后把路径上的点的子树大小增加。
接着将这个位置原本的树按权值分裂成两棵,然后放在插入节点的两个儿子。
删除
我们将
由于只要删除一个数,我们将中间部分的左右儿子合并,然后将剩余的部分合并。
void del(int val){
int L, mid, R;
SplitVal(root, val, L, R);
SplitVal(L, val - 1, L, mid);
mid = merge(d[mid].ls, d[mid].rs);
root = merge(merge(L, mid), R);
}
我们用相似的方式进行优化。
因为题目保证删除的点存在(不保证就找到后再扫一遍),我们直接将路径上的子树大小修改。
然后将删除点的两个子树拼起来,放在删除点的原本位置。
void del(int val){
int*u =&root;
for(;*u && d[*u].val != val;u = &(val < d[*u].val ? d[*u].ls : d[*u].rs))--d[*u].size;
if(u) *u = merge(d[*u].ls, d[*u].rs);
}
查询部分
可以像开头 BST 的那样询问,也可以像这里借助分裂合并(常数较大):
//我们将 < x 的分裂出,然后一直往右走,走到头就是前驱。
int pre_val(int x){
int L, R, now;
SplitVal(root, x - 1, L, R), now = L;
while(d[now].rs)now = d[now].rs;
return root = merge(L, R), d[now].val;
}
//类似
int next_val(int x){
int L, R, now;
SplitVal(root, x, L, R), now = R;
while(d[now].ls)now = d[now].ls;
return root = merge(L, R), d[now].val;
}
//将 < x 的分裂,答案就是这棵树的大小。
int query_rank(int val){
int L, R;
SplitVal(root, val - 1, L, R);
int ans = d[L].size + 1;
return root = merge(L, R), ans;
}
//和 BST 中一样
int kth(int k, int rak){
while(1){
if (rak <= d[d[k].ls].size)k = d[k].ls;
else if (!(rak -= d[d[k].ls].size + 1))return d[k].val;
else k = d[k].rs;
}
}
完整代码
P3369 普通平衡树。
P3369 插入删除优化
P6136 普通平衡树加强版。
序列操作
我们可以将树的中序遍历看做序列顺序的。
然后用按排名分裂,分成
给中间的块打上标记,之后在访问的时候 pushdown
即可。
例题:
可持久化
不知道什么是可持久化的看这:可持久化数据结构简介。
打上注释的是添加的操作。
void split(int now, int val, int&L, int&R){
if(!now)return void(L = R = 0);
int w;
if(d[now].val <= val){
d[L = newnode(1)] = d[now];//
split(d[now].rs, val, d[L].rs, R), getsize(L);
}
else {
d[R = newnode(1)] = d[now];//
split(d[now].ls, val, L, d[R].ls), getsize(R);
}
return getsize(L);
}
int merge(int L, int R){
if(!L || !R)return L + R;
int w;
if(d[L].rank < d[R].rank){
d[w = newnode(1)] = d[L];//
return d[w].rs = merge(d[w].rs, R), getsize(w), w;
}
else {
d[w = newnode(1)] = d[R];//
return d[w].ls = merge(L, d[w].ls), getsize(w), w;
}
}
记得用一个数组存一下每个版本的根。
例题
P3835 可持久化平衡树
模版,直接往上套。
{% folding blue::代码 %}
由于借鉴了题解,我又懒得重写,所以这里的码风不好
洛谷云剪贴板
P5055 可持久化文艺平衡树
打个标记就好了。
洛谷云剪贴板
P5350 序列
挺烦的一道题,要定期重构,遍历完后再清空节点数,pushdown 也有新建节点。
洛谷云剪贴板
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律