Treap(树堆)是一种 OI 常用的平衡树结构,他结合了 BST(二叉查找树)和二叉堆的性质,并通过随机化让平衡树更加平衡,以达到优秀的查找和维护离散数据的效率。
常见的 Treap 写法需要旋转操作来维护平衡树的堆性质,这使得维持树形结构的方式略显复杂,而 FHQ_Treap 则去除了旋转操作,并更加符合平衡树所维护的线性拓扑结构,使得他在极大程度上扩展其功能的同时,代码量极低,这也是其相较于 Splay(伸展树)的优点。
FHQ_Treap 引入了子树的分裂和合并操作,使之在拓扑结构上更加贴近线性表(链表)的结构,而平衡树的快速查找优点也可以保留。据此,FHQ_Treap 还可以更加方便地维护区间信息,同线段树一样,因此线段树的区间类型 FHQ_Treap 都可以兼容。
基本操作:
- 分裂:将一棵子树从其线性映射的某个位置分成两棵子树。
- 合并:将两段不同的子树按照线性相连的方式合并成一棵新的子树。
FHQ_Treap 的堆性质的随机关键字是静态的(从该节点被创建起就不会改变),这一点特点更加利于我们在其中维护区间信息和调试。
用 FHQ_Treap 维护简单有序列表——实现 std::set
功能
由于需要维护 BST 和 Heap 的性质,每个节点分配如下资源:
- 数据
data
:用于维护该位置的信息。 - 点权值
pri
:用于维护随机化的堆性质。 - 左右指针
*ls
*rs
:用于指向该节点左右节点的资源地址。 - 子树大小
size
:用于按位置分裂和查询。
观察每棵子树,他们所具有的如下操作都是一致的:
- 构造:随机分配点权值,分配该节点的数据,左右子节点的指针。
- 分裂:根据需要,可以分为按位置分裂和按值分裂。
- 合并:将两棵相邻的子树合并成一棵更大的新子树。
据此,我们可以大致定义出这个子树类:
1 | template <typename T> |
注意到每次修改之后的子树大小等信息可能会发生改变,因此我们定义一个函数用于处理这些变化:
1 | void flush() { |
在构造函数中加入 flush()
以更新当前节点的信息。
1 | /* construction */ |
分裂
按值分裂:
1 | pair<node*,node*> split(const S &v) { |
按大小分裂:
由于我们在实现这种可以分裂的树的时候不可避免地直接操作指针,因此为了代码简洁和防止非法指针操作,定义函数 get_size
来获取一个指针对应的子树的大小:
1 | template <typename T> |
1 | pair<node*,node*> split(int p) { |
合并
1 | static node *merge(node *l, node *r) { |
这样我们就实现了 std::set
的基本功能。
区间维护
FHQ_Treap 的优势主要体现在其区间操作上,我们仿照线段树的定义形式,给这棵平衡树添加区间信息的维护方式。
变更
- 添加
seg
信息表示子树对应区间。 - 添加
release()
recieve()
modify()
query()
等线段树功能函数。 - 在
flush()
中添加对区间信息的维护。
1 | template <typename S, typename L> |
另:FHQ_Treap 显然具备良好的可持久化条件,读者可以思考如何利用已有的代码实现可持久化的数据结构。(提示:可以从指针的变化情况来考虑,不需要修改现在的函数内容就可以实现)
总结
FHQ_Treap 是一种优秀的数据结构,本文探讨了其结构的优秀特征,并探讨了他在区间信息维护上的优势,并提供了代码各部分的解析。
希望本文能够引发读者的思考,并给读者带来关于 FHQ_Treap 的更多理解。