浅析FHQ-treap

· · 算法·理论

前言

本文无重大错误不再更新,更新会放在博客
fhq-treap 又名“无旋 treap”,有着码量小,易理解,可持久化等特点。
但 fhq-treap 的常数较大。
必要:

选要:

如果这个节点的权值 \le k

反之同理。 #### 按排名分裂 同理,只不过要判断左儿子的大小关系。 ```c++ 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); } ``` ### 合并 合并是分裂的逆操作,我们回到之前的这幅图。 ![图](https://cdn.luogu.com.cn/upload/image_hosting/h67xz3oh.png) 如果要合并,我们需要保证 **$\forall x \in L, y \in R, val(x) \le val(y)$**。 由于两棵树有序,只需要根据优先级考虑哪颗树放“上面”,哪棵树放“下面”,即考虑哪棵树成为子树。 同时,我们还需要满足二叉搜索树的性质。 所以若 $L$ 的根结点的优先级 $< R$ 的,则 $L$ 成为根结点,由于 $R$ 的权值 $\ge L$,所以 $R$ 和 $L$ 的右子树合并; 反之,则 $R$ 作为根结点,$L$ 和 $R$ 的左子树合并。 ```c++ 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; } } ``` ### 其他 有了分裂和合并,剩下的函数就很好实现了。 #### 插入 新建的节点的权值的 $val$,我们将 $\le val$ 和 $> val$ 的分裂,然后将新建的节点合并。 ```c++ void insert(int val){ int L, R; SplitVal(root, val, L, R); root = merge(merge(L, newnode(val)), R); } ``` 这样我们需要调用一次分裂和两次合并,常数明显很大。 我们可以优化: ```c++ void insert(int val){ int u = root, z = newnode(val), f = 0; for(;u && (d[u].rank < d[z].rank);f = u, u = (val < d[u].val ? d[u].ls : d[u].rs))++d[u].size; if(f){ if(val < d[f].val)d[f].ls = z; else d[f].rs = z; } else root = z; SplitVal(u, val, d[z].ls, d[z].rs), getsize(z); } ``` 我们可以用循环寻找我们要插入的位置,然后把路径上的点的子树大小增加。 接着将这个位置原本的树按权值分裂成两棵,然后放在插入节点的两个儿子。 #### 删除 我们将 $< val$、$= val$ 和 $> val$,的部分分裂出来,最中间的那棵树就是要删除的。 由于只要删除一个数,我们将中间部分的左右儿子合并,然后将剩余的部分合并。 ```c++ 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); } ``` 我们用相似的方式进行优化。 因为题目保证删除的点存在(不保证就找到后再扫一遍),我们直接将路径上的子树大小修改。 然后将删除点的两个子树拼起来,放在删除点的原本位置。 ```c++ void del(int val){ int u = root, f = 0; for(;u && d[u].val != val;f = u, u = (val < d[u].val ? d[u].ls : d[u].rs))--d[u].size; if(u){ u = merge(d[u].ls, d[u].rs); if(f){ if(val < d[f].val)d[f].ls = u; else d[f].rs = u; } else root = u; } } ``` #### 查询部分 可以像开头 BST 的那样询问,也可以像这里借助分裂合并(常数较大): ```c++ //我们将 < 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; } ``` ### 完整代码 [P3369 普通平衡树](https://www.luogu.com.cn/paste/atfeqy7n)。 [P3369 插入删除优化](https://www.luogu.com.cn/record/193967730)。 [P6136 普通平衡树加强版](https://www.luogu.com.cn/paste/c2b8gtq2)。 ## 序列操作 我们可以将树的中序遍历看做序列顺序的。 然后用**按排名分裂**,分成 $[1, l - 1], [l, r], [r + 1, n]$ 三块。 给中间的块打上标记,之后在访问的时候 `pushdown` 即可。 [P3391 文艺平衡树代码](https://www.luogu.com.cn/paste/xra1hbdh)。 [P2042 维护序列](https://www.luogu.com.cn/paste/17g38e9q)。 ## 可持久化 不知道什么是可持久化的看这:[可持久化数据结构简介](https://fush-git.github.io/2024/12/10/可持久化数据结构是什么/)。 打上注释的是添加的操作。 ```c++ 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 可持久化平衡树代码](https://www.luogu.com.cn/paste/b6v7j82i)