Treap
Treap 是一种结合了 二叉搜索树(Binary Search Tree) 和 二叉堆(Binary Treap) 特性的高级平衡数据结构,其名称正是由这两个词组合而成:Tree + Heap \(Rightarrow\) Treap。
更具体地说,Treap 中的每个节点都存储了一个二元组 \((X,Y)\):
- 键值 \(X\):满足 二叉搜索树 的性质,即对于任意节点 \((X_0,Y_0)\),其左子树中所有节点的键值 \(X \le X_0\),右子树中所有节点的键值 \(X_0 \le X\)。
- 优先级 \(Y\):满足 二叉堆 的性质(例如大根堆),即任意节点其左右子树中所有节点的优先级 \(Y \le Y_0\)。
Treap 在几何学和特定算法语境下也被称为 笛卡尔树(Cartesian Tree),因为它可以非常直观地嵌入到直角坐标系中,该结构由 Raimund Seidel 和 Cecilia Aragon 于 1989 年提出。
在普通的二叉搜索树(BST)中,如果输入的数据是有序的(例如递增序列),BST 会退化成一条极其低效的 单链。此时,树的深度变为 \(O(N)\),基本的增删改查操作复杂度也会全面退化到 \(O(N)\)。
Treap 引入了 优先级(Priority) \(Y\) 来解决这一痛点:
- 当所有节点的键值 \(X\) 和优先级 \(Y\) 均互不相同时,能够同时满足 BST 性质和堆性质的二叉树结构是 唯一确定 的。也就是说,树的最终形态与数据的插入顺序完全无关。
- 如果 随机生成 每个节点的优先级 \(Y\),那么这棵树在统计学意义上会趋向于一棵高度平衡的随机二叉树,这使得其各项核心操作的平均时间复杂度均能稳定在 \(O(\log N)\)。
- 因此,Treap 也常常被称为“随机化二叉搜索树(Randomized Binary Search Tree)”。
在算法竞赛实现中,通常使用“非旋转式(无旋)Treap”。它不依赖传统的树旋转,而是通过“分裂”和“合并”这两个核心辅助操作来完成所有复杂的增删改工作。
- 分裂操作:\(\text{Split}(T,X)\)
- 定义:将一棵 Treap \(T\) 按照键值 \(X\) 切分为两棵独立的子树 \(L\) 和 \(R\),其中 \(L\) 中所有节点的键值 \(X_L \le X\),而 \(R\) 中所有节点的键值 \(X_R \gt X\)。
- 递归实现逻辑
- 如果当前根节点的键值 \(\le X\),说明该根节点及其左子树必定属于 \(L\)。需要递归对 右子树 调用 \(\text{Split}\),将其切分为 \(L'\) 和 \(R'\)。最终,\(L\) 由“原根节点、其原左子树、以及 \(L'\)”组合而成,而 \(R = R'\)。
- 如果当前根节点的键值 \(\gt X\),说明该根节点及其右子树必定属于 \(R\)。需要递归对 左子树 调用 \(\text{Split}\),将其切分为 \(L'\) 和 \(R'\)。最终,\(R\) 由“原根节点、其原右子树、以及 \(R'\)”组合而成,而 \(L = L'\)。
- 合并操作:\(\text{Merge}(T_1, T_2)\)
- 前置假设:要求 \(T_1\) 中所有节点的键值 \(X\) 必须 小于 \(T_2\) 中所有节点的键值。
- 实现逻辑:为了在合并时不破坏堆的性质,需要比较 \(T_1\) 和 \(T_2\) 根节点的优先级 \(Y\)
- 选择优先级 \(Y\) 较大的那个根节点作为合并后的新根。
- 如果 \(T_1\) 的根节点优先级较高,新根即为 \(T_1\) 的根。保持其左子树不变,将其 右子树 与 \(T_2\) 递归调用 \(\text{Merge}\)。
- 如果 \(T_2\) 的根节点优先级较高,新根即为 \(T_2\) 的根。保持其右子树不变,将其 左子树 与 \(T_1\) 递归调用 \(\text{Merge}\)。
有了这两个基础部件,插入和删除将变得非常简洁。
- 插入 \(\text{Insert}(X,Y)\):直接将原树按照 \(X\) 进行 \(\text{Split}\) 得到 \(L\) 和 \(R\),然后创建一个仅包含 \((X,Y)\) 的孤立节点,先后与 \(L\) 和 \(R\) 进行两次 \(\text{Merge}\) 组合即可。
- 删除 \(\text{Erase}(X)\):可以通过两次 \(\text{Split}\) 将包含 \(X\) 的单个节点隔离出来,然后将其余部分的 Treap 进行 \(\text{Merge}\) 合并。
为了让 Treap 支持更多实用功能(如:在 \(O(\log N)\) 时间内 寻找第 \(K\) 大的元素,或者查询某个元素在有序列表中的 排名/索引),通常需要在节点中增加一个字段 \(s\),用来存储 以该节点为根的子树中的总节点数量。
当树的结构因插入、删除、分裂或合并而发生改变时,受影响节点的 \(s\) 值必须及时更新。
- 编写更新函数,根据当前节点左右孩子的 \(s\) 动态计算自身,即 \(s(L) + s(R) + 1\)。
- 只需在分裂、合并操作的 递归回溯末尾,加入对更新函数的调用,即可确保全树所有节点的子树大小维持最新的正确状态。
例题:P3369 【模板】普通平衡树
动态维护一个可重集合 \(M\),进行 \(n \ (1 \le n \le 10^5)\) 次操作,需要实现以下 6 种操作:
- 插入数 \(x \ (|x| \le 10^7)\)。
- 删除数 \(x\)(若有多个相同的数,仅删除一个)
- 查询数 \(x\) 的排名(即定义为:小于 \(x\) 的数的个数 \(+1\))
- 查询排名为 \(x\) 的数
- 求 \(x\) 的前驱(定义为小于 \(x\) 且最大的数)
- 求 \(x\) 的后继(定义为大于 \(x\) 且最小的数)
- 插入 \(x\):按值 \(x\) 将根树分裂为 \(a \ (\le x)\) 和 \(b \ (\gt x)\),新建一个节点(值为 \(x\)),先与 \(a\) 合并,然后再与 \(b\) 合并。
- 删除 \(x\):先按值 \(x\) 将根树分裂为 \(a \ (\le x)\) 和 \(c \ (\gt x)\),再按值 \(x-1\) 将 \(a\) 分裂为 \(a \ (\lt x)\) 和 \(b \ (=x)\)。此时,树 \(b\) 中所有的值都正好等于 \(x\)。若想只删除一个 \(x\),只需将树 \(b\) 的根节点舍弃,把它的左右子节点合并起来,最后再将 \(a,b,c\) 重新合并回去。
- 查询 \(x\) 的排名:按值 \(x-1\) 分裂为 \(a \ (\lt x)\) 和 \(b \ (\ge x)\),此时,严格小于 \(x\) 的数的个数为 \(s_a\),所以它的排名就是 \(s_a + 1\),查询完毕后将 \(a\) 和 \(b\) 合并还原。
- 查询排名为 \(x\) 的数:直接根据子树大小 \(s\) 在树上进行二分查找,如果左子树的大小大于等于排名 \(x\),则第 \(x\) 小一定在左子树中;如果刚好等于左子树大小 \(+1\),则当前节点就是答案;否则右移并更新排名需求 \(x \gets x - s_l - 1\)。
- 前驱:前驱是严格小于 \(x\) 的最大数,按 \(x-1\) 将根数分裂为 \(a \ (\le x)\) 和 \(b \ (\gt x)\)。树 \(a\) 中最大的数就是 \(a\) 中排名为 \(s_a\) 的数,调用操作 4 即可。
- 后继:后继是严格大于 \(x\) 的最小数,按 \(x\) 将根树分裂为 \(a \ (\le x)\) 和 \(b \ (\gt x)\)。树 \(b\) 中最小的数就是 \(b\) 树中排名为 \(1\) 的数,调用操作 4 即可。
参考代码
#include <cstdio>
#include <random>
#include <chrono>
using namespace std;
const int N = 100005;
int root, val[N], l[N], r[N], sz[N], tot, pri[N];
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
void update(int p) {
sz[p] = sz[l[p]] + sz[r[p]] + 1;
}
void split(int p, int v, int& a, int& b) {
if (!p) {
a = b = 0;
return;
}
if (val[p] <= v) {
a = p;
split(r[p], v, r[a], b);
} else {
b = p;
split(l[p], v, a, l[b]);
}
update(p);
}
int create(int v) {
val[++tot] = v;
pri[tot] = rnd();
sz[tot] = 1;
l[tot] = r[tot] = 0;
return tot;
}
int merge(int a, int b) {
if (!a || !b) return a + b;
if (pri[a] > pri[b]) {
r[a] = merge(r[a], b);
update(a);
return a;
} else {
l[b] = merge(a, l[b]);
update(b);
return b;
}
}
int kth(int p, int k) {
while (p) {
if (k <= sz[l[p]]) p = l[p];
else if (k == sz[l[p]] + 1) return val[p];
else {
k -= sz[l[p]] + 1;
p = r[p];
}
}
return 0;
}
int main()
{
int n; scanf("%d", &n);
while (n--) {
int op, x; scanf("%d%d", &op, &x);
if (op == 1) {
int a, b;
split(root, x, a, b);
root = merge(merge(a, create(x)), b);
} else if (op == 2) {
int a, b, c;
split(root, x, a, c);
split(a, x - 1, a, b);
if (b) b = merge(l[b], r[b]);
root = merge(merge(a, b), c);
} else if (op == 3) {
int a, b;
split(root, x - 1, a, b);
printf("%d\n", sz[a] + 1);
root = merge(a, b);
} else if (op == 4) {
printf("%d\n", kth(root, x));
} else if (op == 5) {
int a, b;
split(root, x - 1, a, b);
printf("%d\n", kth(a, sz[a]));
root = merge(a, b);
} else {
int a, b;
split(root, x, a, b);
printf("%d\n", kth(b, 1));
root = merge(a, b);
}
}
return 0;
}

浙公网安备 33010602011771号