AcWing 253:普通平衡树 ← FHQ-Treap 树(结构体 + split + merge)
【题目来源】
https://www.luogu.com.cn/problem/P3369
https://www.acwing.com/problem/content/255/
【题目描述】
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1. 插入数值 x。
2. 删除数值 x(若有多个相同的数,应只删除一个)。
3. 查询数值 x 的排名(若有多个相同的数,应输出最小的排名)。
4. 查询排名为 x 的数值。
5. 求数值 x 的前驱(前驱定义为小于 x 的最大的数)。
6. 求数值 x 的后继(后继定义为大于 x 的最小的数)。
注意: 数据保证查询的结果一定存在。
【输入格式】
第一行为 n,表示操作的个数。
接下来 n 行每行有两个数 opt 和 x,opt 表示操作的序号(1≤opt≤6)。
【输出格式】
对于操作 3,4,5,6 每行输出一个数,表示对应答案。
【数据范围】
1≤n≤100000,所有数均在 −10^7 到 10^7 内。
【输入样例】
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
【输出样例】
106465
84185
492737
【算法分析】
(一)FHQ−Treap 简介
● FHQ−Treap,也称非旋 Treap,由范浩强提出。顾名思义,FHQ−Treap 就是不需要通过旋转,而是通过分裂(split)与合并(merge)维护的 Treap。FHQ−Treap 与 Treap 的另外一个区别是 FHQ−Treap 可持久化。
● FHQ-Treap 的高明之处在于所有的操作都只用到了分裂(split)与合并(merge)这两个基本操作。
● 本题的 Treap 树实现参见:https://blog.csdn.net/hnjzsyjyj/article/details/138482439
● 本题的替罪羊树实现参见:https://blog.csdn.net/hnjzsyjyj/article/details/128647972
(二)核心代码解析
● pushup:更新子树大小
(1)作用:当节点的左右子树发生变化(如分裂、合并、插入)时,更新当前节点的 size;
(2)为什么需要:size 是实现 “查排名、查第 k 小” 的核心依据(比如左子树大小就是 “比当前节点小的节点数”);
(3)调用时机:分裂(split)、合并(merge)操作后,必须调用 pushup 保证 size 正确。
● get_node:创建新节点
(1)作用:封装 “创建节点” 的逻辑,避免重复代码;
(2)随机 pri 的关键:通过随机数让树的结构随机化,从而保证 Treap 的平衡(平均高度为
O(logn))。
● split:按值分裂 Treap
(1)核心逻辑:递归按 BST 性质拆分树,把 “<=v” 的节点归到 x,“>v” 的归到 y;
(2)引用参数 &x,&y:因为要修改调用方的变量(比如 insert 里的 x/y),必须用引用;
(3)举例:若树中有 [84185, 89851, 106465],以 v=100000 分裂,x 是 [84185,89851],y 是 [106465]。
● merge:合并两个 Treap
(1)核心逻辑:按堆性质(pri)决定父节点,按 BST 性质(val)决定子树位置;
(2)前提条件:x 的所有 val <= y 的所有 val(由 split 保证,所以 merge 是安全的);
(3)堆性质的作用:通过随机 pri 让合并后的树保持平衡,避免退化成链表。
● insert:插入数值 u
(1)步骤拆解:
分裂原树为 x(<=u)和 y(>u);
创建新节点存储 u;
先合并 x 和新节点,再合并结果和 y,得到新树;
(2)举例:插入 106465 时,原树为空,split 后 x=0、y=0,合并后 root 就是新节点的编号。
● remove:删除数值 u
(1)核心思路:精准定位 “val=u” 的节点(y),通过合并其左右子树跳过 y,实现删除;
(2)步骤拆解:
第一次 split:把原树分成 <=u(x)和>u(z);
第二次 split:把 x 分成 <u(x)和 =u(y);
合并 y 的左右子树(相当于删除 y 节点);
重新合并 x、删后的 y、z,得到删除后的树;
(3)注意:此代码仅删除一个 u(若有重复值,需调整 split 逻辑)。
● get_rank_by_val:查询 x 的排名
(1)原理:排名的定义是 “比 x 小的数的个数 +1”,而 tr[x].size 就是 “比 val 小的数的个数”;
(2)关键:split 用 val-1 作为阈值,确保 x 中只有 <val 的节点;
(3)举例:查询 106465 的排名时,原树只有 106465,split (val-1=106464) 后 x 为空(size=0),排名 = 0+1=1。
● get_val_by_rank:查询第 k 小的数
(1)核心逻辑:利用 BST 性质和 size 定位第 k 小:
左子树 size = 比当前节点小的节点数;
若 rank ≤ 左子树 size → 目标在左子树;
若 rank = 左子树 size +1 → 目标是当前节点;
否则 → 目标在右子树,rank 减去左子树 + 自身的数量。
(2)举例:查询第 1 小的数时,根节点左子树 size=0 < 1,且 0+1=1,直接输出根节点的 val(106465)。
● get_prev:查询 x 的前驱(比 x 小的最大数)
(1)原理:前驱是 “<v 的最大数”,而 BST 中 “最右节点” 就是最大值;
(2)步骤:
split 出所有 <v 的节点(x);
遍历 x 找到最右节点(val 最大);
输出该节点的 val,合并恢复原树;
(3)举例:查询 493598 的前驱时,split (v-1=493597) 后 x 包含[84185,89851,106465,317721,460929,492737],最右节点是 492737。
● get_next:查询 x 的后继(比 x 大的最小数)
(1)原理:后继是 “>v 的最小数”,BST 中 “最左节点” 就是最小值;
(2)步骤:
split 出所有 >v 的节点(y);
遍历 y 找到最左节点(val 最小);
输出该节点的 val,合并恢复原树;
(3)举例:查询 81968 的后继时,split (v=81968) 后 y 包含 [84185,89851,106465,...],最左节点是 84185。
【算法代码】
【参考文献】

浙公网安备 33010602011771号