信奥赛C++提高组csp-s之平衡树(Treap)

平衡树概述
为什么需要平衡树?
二叉查找树(Binary Search Tree, BST)的查找、插入、删除操作时间复杂度为 O(h),其中 h 为树的高度。在理想情况下,BST 的高度为 O(log n),但在最坏情况下(例如插入的节点序列本身有序),BST 会退化成单链表,性能下降到 O(n)。
平衡树的核心思想是在进行插入和删除操作时,通过旋转或重构等方式保持树的高度始终维持在 O(log n) 量级,从而保证所有操作的时间复杂度稳定在 O(log n)。
常见平衡树类型
常见的平衡树包括:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| Treap(树堆) | 利用随机优先级维持堆性质,简单易写 | 权值平衡树(P3369)、可持久化(P3835) |
| Splay | 通过伸展操作将访问节点旋至根,常数较大但功能强大 | 区间操作(P3391)、序列维护(P2596) |
| 替罪羊树 | 通过暴力重构维持平衡,无需旋转 | 代码简单、避免复杂旋转逻辑 |
| SBT | 通过维护子树大小实现平衡 | 动态顺序统计 |
其中,Treap 是CSP-S考场中最常用的平衡树。本文将以 Treap 为核心进行讲解。
Treap核心知识点详解
Treap(树堆)是一种结合了二叉搜索树(BST)和堆 特性的平衡树结构。它通过为每个节点赋予一个随机的优先级(pri),并利用旋转操作来同时维护BST的有序性和堆的优先级,从而实现了期望的平衡复杂度,非常适合CSP-S竞赛中快速编写和调试。
1. 数据结构定义
cpp
// 用数组模拟静态树,速度更快
int v[N]; // 节点的值 (key)
int p[N]; // 随机优先级 (priority)
int l[N]; // 左儿子
int r[N]; // 右儿子
int sz[N]; // 子树大小
int ct[N]; // 当前值的重复个数
- 存储方式:采用数组模拟而非结构体或指针,这能大幅提升运行效率。
- 逻辑结构:每个节点存储
v值来维护BST性质,同时存储一个随机生成的p值来维护大根堆性质。 - 子树大小
sz:用于高效地查询排名和第k大的数。当节点值重复时,我们将其计数ct累加,而不是插入多个相同节点,这能显著简化删除操作并节省空间。
2. 核心操作原理
| 操作 | 主要代码 | 原理说明 |
|---|---|---|
| 旋转 | void rot(int &u, int d) |
在不破坏BST中序遍历顺序的前提下,通过左旋/右旋改变父子关系。例如,右旋会将左子节点提升为父节点,以维护p值的堆性质。 |
| 插入 | void ins(int &u, int x) |
1. 若节点为空,创建新节点。 2. 若x等于当前v[u],增加计数ct。 3. 若x小于v[u],递归插入左子树;反之插入右子树。 4. 回溯时,若子节点优先级p大于当前节点p,则执行旋转。 |
| 删除 | void del(int &u, int x) |
1. 若x等于v[u]且计数ct[u] > 1,则直接减少计数。 2. 若计数为1,则需要删除节点: a. 若为叶子节点或只有一个子节点,直接删除。 b. 若有两个子节点,则将优先级较高的子节点旋转上来,然后递归删除。 |
| 查排名 | int rk(int u, int x) |
1. 若x < v[u],向左子树递归。 2. 若x == v[u],排名为 sz[l[u]] + 1。 3. 若x > v[u],排名为 sz[l[u]] + ct[u] + rk(r[u], x)。 |
| 查第k大 | int kth(int u, int k) |
1. 若 k <= sz[l[u]],在左子树中找。 2. 若 sz[l[u]] < k <= sz[l[u]] + ct[u],答案就是v[u]。 3. 否则,在右子树中找第 k - sz[l[u]] - ct[u] 大的数。 |
| 前驱/后继 | int pre(int u, int x) |
利用BST性质递归查找:前驱为严格小于x的最大值;后继为严格大于x的最小值。 |
案例研究:普通平衡树
题目描述
您需要动态地维护一个可重集合 M M M,并且提供以下操作:
- 向 M M M 中插入一个数 x x x。
- 从 M M M 中删除一个数 x x x。(若有多个相同的数,应只删除一个)
- 查询 M M M 中有多少个数比 x x x 小,并且将得到的答案加 1 1 1。
- 查询如果将 M M M 从小到大排列后,排名位于第 x x x 位的数。
- 查询 M M M 中 x x x 的前驱(定义为 M M M 中小于 x x x,且最大的数)。
- 查询 M M M 中 x x x 的后继(定义为 M M M 中大于 x x x,且最小的数)。
对于操作 3 , 5 , 6 3,5,6 3,5,6,不保证 当前可重集中存在数 x x x。
对于操作 4 , 5 , 6 4,5,6 4,5,6,保证答案一定存在。
输入格式
第一行为 n n n,表示操作的个数,下面 n n n 行每行有两个数 opt \text{opt} opt 和 x x x, opt \text{opt} opt 表示操作的序号( 1 \\leq \\text{opt} \\leq 6 )。
输出格式
对于操作 3 , 4 , 5 , 6 3,4,5,6 3,4,5,6 每行输出一个数,表示对应答案。
输入输出样例 1
输入 1
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
输出 1
106465
84185
492737
【数据范围】
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 10 5 1\le n \le 10^5 1≤n≤105, ∣ x ∣ ≤ 10 7 |x| \le 10^7 ∣x∣≤107。
思路分析(旋转 Treap)
- 节点设计:每个节点包含左子、右子、值、出现次数、子树大小、随机优先级。子树大小用于快速求排名和第 k 小。
- 旋转操作:右旋和左旋用于维护堆性质(优先级大的在上)。旋转后需更新相关节点的子树大小。
- 插入:递归查找插入位置,若值已存在则增加计数;否则新建节点。插入后若子节点优先级大于父节点,则通过旋转将子节点上提。
- 删除:递归找到目标节点。若计数 >1 则减一;否则若只有一个孩子或没有孩子,则直接替换;若有两个孩子,则通过旋转将优先级大的孩子旋到当前节点位置,再递归删除该值(此时目标值已到旋转后的子树上)。
- 查询排名:根据 BST 性质,比较当前节点值与目标值,递归计算左子树大小并累加。
- 查询第 k 小:利用左子树大小判断目标所在区间,递归查找。
- 前驱/后继:递归查找小于 x 的最大值 / 大于 x 的最小值。
所有操作时间复杂度 O(log n),空间复杂度 O(n)。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
const int INF = 1e9 + 10; // 用于前驱后继的边界
struct Node {
int l, r; // 左右子节点下标
int v; // 节点值
int c; // 该值出现次数
int s; // 子树大小(含重复计数)
int p; // 随机优先级
} t[N];
int rt, tot; // 根节点编号,节点总数
// 更新节点 u 的子树大小
void upd(int u) {
t[u].s = t[t[u].l].s + t[t[u].r].s + t[u].c;
}
// 右旋(以 u 为轴,将左孩子旋上来)
void rot_r(int &u) {
int l = t[u].l;
t[u].l = t[l].r;
t[l].r = u;
upd(u);
upd(l);
u = l;
}
// 左旋(以 u 为轴,将右孩子旋上来)
void rot_l(int &u) {
int r = t[u].r;
t[u].r = t[r].l;
t[r].l = u;
upd(u);
upd(r);
u = r;
}
// 插入值 v,u 为当前子树根引用
void ins(int &u, int v) {
if (!u) {
u = ++tot;
t[u] = {0, 0, v, 1, 1, rand()};
return;
}
if (v == t[u].v) {
t[u].c++;
upd(u);
return;
}
if (v < t[u].v) {
ins(t[u].l, v);
if (t[t[u].l].p > t[u].p) rot_r(u);
} else {
ins(t[u].r, v);
if (t[t[u].r].p > t[u].p) rot_l(u);
}
upd(u);
}
// 删除一个值 v(只删一个)
void del(int &u, int v) {
if (!u) return;
if (v == t[u].v) {
if (t[u].c > 1) {
t[u].c--;
upd(u);
return;
}
// 节点无孩子或只有一个孩子
if (!t[u].l || !t[u].r) {
u = t[u].l + t[u].r;
return;
}
// 两个孩子,将优先级大的孩子旋上来,然后递归删除
if (t[t[u].l].p > t[t[u].r].p) {
rot_r(u);
del(t[u].r, v);
} else {
rot_l(u);
del(t[u].l, v);
}
upd(u);
return;
}
if (v < t[u].v) del(t[u].l, v);
else del(t[u].r, v);
upd(u);
}
// 查询比 v 小的数的个数 +1(即 v 的排名)
int rk(int u, int v) {
if (!u) return 1; // 空树,v 排名为 1
if (v <= t[u].v) return rk(t[u].l, v);
return t[t[u].l].s + t[u].c + rk(t[u].r, v);
}
// 查询排名为 k 的数(k 从 1 开始)
int kth(int u, int k) {
while (u) {
if (k <= t[t[u].l].s) u = t[u].l;
else if (k <= t[t[u].l].s + t[u].c) return t[u].v;
else {
k -= t[t[u].l].s + t[u].c;
u = t[u].r;
}
}
return 0; // 题目保证有解
}
// 查询 v 的前驱(小于 v 的最大数)
int pre(int u, int v) {
int res = -INF;
while (u) {
if (v <= t[u].v) u = t[u].l;
else {
res = max(res, t[u].v);
u = t[u].r;
}
}
return res;
}
// 查询 v 的后继(大于 v 的最小数)
int suc(int u, int v) {
int res = INF;
while (u) {
if (v >= t[u].v) u = t[u].r;
else {
res = min(res, t[u].v);
u = t[u].l;
}
}
return res;
}
int main() {
srand(time(0));
int n;
scanf("%d", &n);
while (n--) {
int op, x;
scanf("%d%d", &op, &x);
if (op == 1) ins(rt, x);
else if (op == 2) del(rt, x);
else if (op == 3) printf("%d\n", rk(rt, x));
else if (op == 4) printf("%d\n", kth(rt, x));
else if (op == 5) printf("%d\n", pre(rt, x));
else if (op == 6) printf("%d\n", suc(rt, x));
}
return 0;
}
功能分析
- 插入 (ins):递归插入,若值已存在则增加计数;否则新建节点。通过旋转维持大根堆性质,保证树高期望 O(log n)。
- 删除 (del):递归删除,若计数 >1 则减一;若只有一个孩子则直接替换;若有两个孩子则通过旋转将优先级大的孩子旋到当前节点,再递归删除目标值。
- 查询排名 (rk):利用 BST 性质,递归统计比 x 小的节点个数,最终结果加 1 即为排名。
- 查询第 k 小 (kth):根据左子树大小与当前节点计数决定往左、返回当前、或往右查找。
- 前驱 (pre):循环查找小于 x 的最大值。
- 后继 (suc):循环查找大于 x 的最小值。
更多系列知识,请查看专栏:《信奥赛C++提高组csp-s知识详解及案例实践》:
https://blog.csdn.net/weixin_66461496/category_13113932.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
信奥赛C++普及组csp-j初赛&复赛真题题解(持续更新) https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新)
https://blog.csdn.net/weixin_66461496/category_13125089.html
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、csp/信奥赛C++,完整信奥赛系列课程(永久学习):
https://edu.csdn.net/lecturer/7901 点击跳转
· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}