前置芝士——二叉搜索树 BST
简介
二叉搜索树是一种二叉树的树形数据结构,其定义如下:
- 空树是二叉搜索树。
- 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
- 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
- 二叉搜索树的左右子树均为二叉搜索树。
操作
二叉搜索树支持 6 个基本操作:
- 插入
- 删除
- 按排名查值
- 按值查排名
- 查前驱(小于它的最大值)
- 查后继(大于它的最小值)
建树
没有什么复杂的操作,但是为了防止宇宙射线导致的越界,可以插入两个值为 $inf$ 和 $-inf$ 的节点。
插入
插入一个值 v,从根节点开始向下递归。
设现在所在节点为 now,分许多情况:
- 如果 now 为空,则在当前位置新建一个值为 v 的节点,并将 cnt 设为 1。
- 如果 $val_{now} == v$,那么将当前位置的 cnt 加一即可。
- 如果 $val_{now} > v$,递归 now 的左子树。
- 如果 $val_{now} < v$,递归 now 的右子树。
删除
参考插入找到要修改的节点 p
- 如果 cnt 大于 1,则直接减一。
- 如果 cnt 等于 1:
- 若为叶节点,直接删除。
- 若仅有一个儿子,删除它并让它的儿子接替位置。
- 若有两个儿子,则让左子树中最大节点或者右子树中最小节点接替它。
按排名查值
这个操作需要预先处理出每个节点为根的子树中的 cnt 之和(下文用 siz)。
假设要查找第 k 小的数,p 为当前节点,$p_l$ 为左儿子,$p_r$ 为右儿子。
每当到达一个节点 p(从根开始):
- 若 $k \le siz_{p_l}$,则返回左子树中第 k 小的值。
- 若 $siz_{p_l} < k \le siz_{p_l}+cnt_p$,则返回 p 节点的值。
- 否则返回右子树中第 $k-siz_{p_l}-cnt_p$ 小的值。
按值查排名
类似插入时的查找方法,统计途径节点的 cnt 和其左子树的 siz 之和直至当前节点与查找值相同(不加但当前点的 cnt),对累和加一就是排名。
查前驱/后继
前驱和后继查找方法类似,以前驱为例。
尝试使用 while 循环解决,能往右子树走就往右子树走(求后继时反之),最后一个不为空的点即为前驱。
int get_pre(int v) {
int now = root, pre;
while (now) {
if (v <= val[now]) now = son[now][0];
else {
pre = val[now];
now = son[now][1];
}
}
return pre;
}
小结
二叉搜索树非常强大,处理纯随机数据时,每次操作时间复杂度为 $O(logn)$,但是如果人为造数据的话,可能会退化成链,导致时间复杂度退化成 $O(n)$。
平衡树
平衡树有好多种,边学边写吧~。
目录
序号 | 类型 |
---|---|
1 | Treap |
2 | Splay |
3 | FHQ Treap |
4 | 红黑树 |
5 | 替罪羊树 |
6 | Link Cut Tree |
概念
通过特殊的处理使 BST 不会退化成链,并使结构“平衡”(每个节点的左右子树尽量接近,以使操作复杂度可观)。通过该操作构建的 BST 就称作平衡树。
Treap
简介
Treap 就是把二叉搜索树 (Tree) 和堆 (Heap) 结合(名字亦是如此)。每个节点有两个值, 一个是该节点的值,一个是随机生成的优先级。以值为关键字,排列方式满足二叉搜索树的性质的同时,以优先级为关键字,排列方式满足堆的性质。也就是说,维护的节点将同时满足堆和二叉搜索树的性质和结构。
由于堆必定不会退化为链,因此 Treap 不会退化为链。那么可以保证操作时间复杂度维持在 $O(log n)$。
所以,每加入一个节点,就要不停旋转直到它满足满足堆的性质,以保证树的平衡。
操作
旋转 P.S. 关键操作
分为左旋和右旋,可以借图理解。
以左旋为例子,当前节点为 p,则做tmp = son[p][1],son[p][1]=son[tmp][0],son[tmp][0]=p
(右旋把左右儿子部分反一下)
void Rotate(int &u, int d) { //旋转 0-左旋 1-右旋
int tmp = son[u][d ^ 1];
son[u][d ^ 1] = son[tmp][d];
son[tmp][d] = u;
PushUp(u);
PushUp(tmp);
u = tmp;
return ;
}
插入
按照 BST 的插入方式插入,如果原本没有该值的节点则随机分配一个优先级。完成后再维护堆的性质,不过上移过程由单纯的 swap 变成上述旋转操作。
删除
(如果有多个,cnt 减一即可,否则)与堆一样,将节点下旋到叶子再删去。
其他
与 BST 一样。
源代码
/**********************************
Problem: luogu - P3369 - 【模板】普通平衡树
State: Accepted
From: 【模板】
Algorithm: treap
Author: __shadow__
Last updated on 2023.12.11
**********************************/
#include <cstdio>
#include <stdlib.h>
const int N = 1e5 + 5;
struct Treap {
int val[N], key[N], son[N][2], siz[N], cnt[N], tot, root;
int New(int v) {
val[++ tot] = v;
key[tot] = rand();
siz[tot] = 1;
cnt[tot] = 1;
return tot;
}
void PushUp(int u) {
siz[u] = siz[son[u][0]] + siz[son[u][1]] + cnt[u];
}
void Rotate(int &u, int d) { //旋转 0-左旋 1-右旋
int tmp = son[u][d ^ 1];
son[u][d ^ 1] = son[tmp][d];
son[tmp][d] = u;
PushUp(u);
PushUp(tmp);
u = tmp;
return ;
}
void insert(int &u, int v) {
if (!u) {
u = New(v);
return ;
}
if (v == val[u]) {
++ cnt[u];
PushUp(u);
return ;
}
int d = v > val[u];
insert(son[u][d], v);
if (key[u] < key[son[u][d]]) Rotate(u, d ^ 1);
PushUp(u);
return ;
}
void Remove(int &u, int v) {
if (!u) return ;
if (v == val[u]) {
if (cnt[u] > 1) {
-- cnt[u];
PushUp(u);
return ;
}
if (son[u][0] || son[u][1]) {
if (!son[u][1] || key[son[u][0]] > key[son[u][1]]) {
Rotate(u, 1);
Remove(son[u][1], v);
}
else {
Rotate (u, 0);
Remove(son[u][0], v);
}
PushUp(u);
return ;
}
else u = 0;
return ;
}
Remove(son[u][(v > val[u])], v);
PushUp(u);
return ;
}
int get_rk(int u, int v) {
if (!u) return 1;
if (v == val[u]) return siz[son[u][0]] + 1;
if (v < val[u]) return get_rk(son[u][0], v);
return get_rk(son[u][1], v) + siz[son[u][0]] + cnt[u];
}
int get_val(int u, int rk) {
if (!u) return 0;
if (rk <= siz[son[u][0]]) return get_val(son[u][0], rk);
if (rk <= siz[son[u][0]] + cnt[u]) return val[u];
return get_val(son[u][1], rk - siz[son[u][0]] - cnt[u]);
}
int get_pre(int v) {
int now = root, pre;
while (now) {
if (v <= val[now]) now = son[now][0];
else {
pre = val[now];
now = son[now][1];
}
}
return pre;
}
int get_nxt(int v) {
int now = root, nxt;
while (now) {
if (v >= val[now]) now = son[now][1];
else {
nxt = val[now];
now = son[now][0];
}
}
return nxt;
}
}tre;
int n;
int main() {
scanf ("%d", &n);
while (n --) {
int opt, x;
scanf ("%d%d", &opt, &x);
if (opt == 1) tre.insert(tre.root, x);
else if (opt == 2) tre.Remove(tre.root, x);
else if (opt == 3) printf ("%d\n", tre.get_rk(tre.root, x));
else if (opt == 4) printf ("%d\n", tre.get_val(tre.root, x));
else if (opt == 5) printf ("%d\n", tre.get_pre(x));
else if (opt == 6) printf ("%d\n", tre.get_nxt(x));
}
return 0;
}
FHQ Treap
简介
FHQ Treap 是基于 Treap 发明的“无旋 Treap”,代码短,易理解还速度快。
操作
FHQ Treap 的核心操作是 分裂 和 合并。就是将一棵二叉查找树按要求分成两棵或把两棵合成一棵。
分裂
常用的是按 val 或者 siz 来分裂。按 val 分裂时,分成小于等于 val 的和大于 val 的两棵树。按 siz 分列时,前 siz 个节点一棵树,剩下的另一棵。
以按 val 的为例子。用四个参数 u、v、x、y,分别表示当前节点,标准 v,分裂出的两个树当前待插入的位置。
到一个节点,有两种情况:
- 若 $val_u \le v$,将 u 接在树 x 下,继续分裂 u 的右子树。
- 若 $val_u > v$,将 u 接在树 y 下,继续分裂 u 的左子树。
Code:
void Split(int u, int v, int &x, int &y) {
if (!u) {
x = y = 0;
return ;
}
if (val[u] <= v) {
x = u;
Split(son[u][1], v, son[u][1], y);
}
else {
y = u;
Split(son[u][0], v, x, son[u][0]);
}
PushUp(u);
return ;
}
合并
将树 x 和 y 合并并返回根节点编号。与 Treap 相同,为防止退化成链,每个节点加入随机优先级 key,要求 key 满足大根堆。
Code:
int Merge(int x, int y) {
if (!x || !y) return x | y;
if (key[x] < key[y]) {
son[y][0] = Merge(x, son[y][0]);
PushUp(y);
return y;
}
else {
son[x][1] = Merge(son[x][1], y);
PushUp(x);
return x;
}
return 0;
}
查询第 k 大
与 Treap 一样,这次用 while 写。
Code:
int get_val(int k) {
int p = root;
while (1) {
if (k <= siz[son[p][0]]) p = son[p][0];
else {
k -= siz[son[p][0]] + 1;
if (!k) return val[p];
p = son[p][1];
}
}
}
其他
其他操作均可使用上述三种操作实现,具体实现见代码。
例题
P3369 【模板】普通平衡树
/**********************************
Problem: luogu - P3369 - 【模板】普通平衡树
State: Accepted
From: 【模板】
Algorithm: FHQ treap
Author: __shadow__
Last updated on 2023.12.11
**********************************/
#include <cstdio>
#include <stdlib.h>
const int N = 1e5 + 5;
struct FHQ {
int root, val[N], key[N], siz[N], son[N][2], tot;
int New(int v) {
val[++tot] = v;
key[tot] = rand();
siz[tot] = 1;
return tot;
}
void PushUp(int u) {
siz[u] = siz[son[u][0]] + siz[son[u][1]] + 1;
return ;
}
void Split(int u, int v, int &x, int &y) {
if (!u) {
x = y = 0;
return ;
}
if (val[u] <= v) {
x = u;
Split(son[u][1], v, son[u][1], y);
}
else {
y = u;
Split(son[u][0], v, x, son[u][0]);
}
PushUp(u);
return ;
}
int Merge(int x, int y) {
if (!x || !y) return x | y;
if (key[x] < key[y]) {
son[y][0] = Merge(x, son[y][0]);
PushUp(y);
return y;
}
else {
son[x][1] = Merge(son[x][1], y);
PushUp(x);
return x;
}
return 0;
}
void insert(int k) {
int tn = New(k);
if (!root) {
root = tn;
return ;
}
int x, y;
Split(root, k, x, y);
root = Merge(Merge(x, tn), y);
return ;
}
void remove(int k) {
int x, y, z;
Split(root, k, x, z);
Split(x, k - 1, x, y);
root = Merge(Merge(x, Merge(son[y][0], son[y][1])), z);
return ;
}
int get_rk(int k) { //rnk(int k)
int x, y;
Split (root, k - 1, x, y);
int ans = siz[x] + 1;
Merge(x, y);
return ans;
}
int get_val(int k) { //kth(int k)
int p = root;
while (1) {
if (k <= siz[son[p][0]]) p = son[p][0];
else {
k -= siz[son[p][0]] + 1;
if (!k) return val[p];
p = son[p][1];
}
}
}
int get_pre(int k) { //pre
return get_val(get_rk(k) - 1);
}
int get_nxt(int k) { //nxt
return get_val(get_rk(k + 1));
}
} tre;
int n;
int main() {
scanf ("%d", &n);
while (n --) {
int opt, x;
scanf ("%d%d", &opt, &x);
if (opt == 1) tre.insert(x);
else if (opt == 2) tre.remove(x);
else if (opt == 3) printf ("%d\n", tre.get_rk(x));
else if (opt == 4) printf ("%d\n", tre.get_val(x));
else if (opt == 5) printf ("%d\n", tre.get_pre(x));
else if (opt == 6) printf ("%d\n", tre.get_nxt(x));
}
return 0;
}
P3391 【模板】文艺平衡树
将序列看作树的中序遍历,那么就可以发现每次区间翻转只需要将该区间的这棵树上每个非叶子节点的左右儿子互换即可。
只需要以子树大小为关键字分裂就好了。同时应用懒标记优化,就得到正解。
P.S. 一个节点被标记时其左右儿子已完成交换。
/**********************************
Problem: luogu - P3391 【模板】文艺平衡树
State: Accepted
From: 【模板】
Algorithm: FHQ treap
Author: __shadow__
Last updated on 2023.12.20
**********************************/
#include <cstdio>
#include <algorithm>
#include <stdlib.h>
const int N = 1e5 + 5;
int n, m;
struct FHQ {
int rt, val[N], key[N], siz[N], son[N][2], tot;
long long sum[N];
bool rev[N];
int New(int v) {
val[++ tot] = v;
key[tot] = rand();
siz[tot] = 1;
sum[tot] = v;
return tot;
}
inline void insert(const int &v) {
int tn = New(v);
if (!rt) {
rt = tn;
return ;
}
rt = merge(rt, tn);
return ;
}
inline void PushUp(const int &u) {
sum[u] = sum[son[u][0]] + sum[son[u][1]] + val[u];
siz[u] = siz[son[u][0]] + siz[son[u][1]] + 1;
}
inline void PushRev(const int &u) {
if (u == 0) return ;
std :: swap(son[u][0], son[u][1]);
rev[u] ^= 1;
return ;
}
inline void PushDown(const int &u) {
if (!rev[u]) return ;
PushRev(son[u][0]);
PushRev(son[u][1]);
rev[u] = false;
return ;
}
void split(int u, int k, int &x, int &y) {
if (!u) {
x = y = 0;
return ;
}
PushDown(u);
if (k > siz[son[u][0]]) {
x = u;
split(son[u][1], k - siz[son[u][0]] - 1, son[u][1], y);
}
else {
y = u;
split(son[u][0], k, x, son[u][0]);
}
PushUp(u);
return ;
}
int merge(int x, int y) {
if (x == 0 || y == 0) return x | y;
PushDown(x);
PushDown(y);
if (key[x] < key[y]) {
son[x][1] = merge(son[x][1], y);
PushUp(x);
return x;
}
else {
son[y][0] = merge(x, son[y][0]);
PushUp(y);
return y;
}
}
void reversal(int l, int r) {
int x, y, z;
split(rt, r, x, z);
split(x, l - 1, x, y);
PushRev(y);
rt = merge(merge(x, y), z);
return ;
}
void print(int u) {
PushDown(u);
if (son[u][0]) print(son[u][0]);
printf ("%d ", val[u]);
if (son[u][1]) print(son[u][1]);
return ;
}
}tre;
int main() {
scanf ("%d%d", &n, &m);
for (int i = 1;i <= n; ++ i) tre.insert(i);
while (m --) {
int l, r;
scanf ("%d%d", &l, &r);
tre.reversal(l, r);
}
tre.print(tre.rt);
return 0;
}
/*
struct Treap { //--Sooke
int rt, val[N], key[N], siz[N], son[N][2];
ll sum[N];
bool rev[N];
inline void pushUp(const int& u) {
sum[u] = sum[son[u][0]] + sum[son[u][1]] + val[u];
siz[u] = siz[son[u][0]] + siz[son[u][1]] + 1;
}
inline void pushRev(const int& u) {
if (u == 0) { return; }
rev[u] ^= 1; std::swap(son[u][0], son[u][1]);
}
inline void pushDown(const int& u) {
if (rev[u]) {
pushRev(son[u][0]); pushRev(son[u][1]);
rev[u] = false;
}
}
void split(int u, int k, int& l, int& r) {
if (u == 0) { l = r = 0; return; }
pushDown(u);
if (siz[son[u][0]] < k) {
l = u; split(son[u][1], k - siz[son[u][0]] - 1, son[u][1], r);
} else {
r = u; split(son[u][0], k, l, son[u][0]);
}
pushUp(u);
}
int merge(int l, int r) {
if (l == 0 || r == 0) { return l | r; }
pushDown(l); pushDown(r);
if (key[l] < key[r]) {
son[l][1] = merge(son[l][1], r); pushUp(l); return l;
} else {
son[r][0] = merge(l, son[r][0]); pushUp(r); return r;
}
}
} tr;
*/
标签:return,val,int,siz,son,平衡,节点
From: https://www.cnblogs.com/Assassins-Creed/p/18018369