现在有一棵以 p 为根的树,要按值分裂成左右两棵,根为 x, y,左边的树的值都小于等于 v,右边的树的值都大于 v。
如果 p 的值 \le v,那么说明 p 及其左子树一定在左边的那棵树,且 p 一定是左边的树的根,那么令 x = p,右子树中可能还有 \le v 的,递归处理右子树,并将右子树的分裂出的左边的树的根放到 p 的右儿子。
如果 p 的值 > v,那么说明 p 及其右子树一定在右边的那棵树,且 p 一定是右边的树的根,那么令 y = p,左子树中可能还有 > v 的,递归处理左子树,并将左子树的右边的树放到 p 的左儿子。
可以发现,这样分裂出的两棵树的边一定是初始时的边或者是祖先连向儿子,所以满足堆的性质。
最后更新一下 p 的子树大小即可。
pair<int, int> split(int p, int v) { // pair 表示 x, y
if (!p) { // 当前节点为空,分裂出的两棵树也都为空
return make_pair(0, 0);
}
pair<int, int> res;
if (tr[p].v <= v) {
res = split(tr[x].s[1], v); // 递归右子树
tr[x].s[1] = res.first; // 将右儿子改为右子树的分裂出的左边的树的根
res.first = p; // 将 x 改为 p
} else {
y = p;
res = split(tr[y].s[0], v); // 递归左子树
tr[y].s[0] = res.second; // 将左儿子改为左子树的分裂出的右边的树的根
res.second = p; // 将 y 改为 p
}
pushup(p); // 更新 p 的 sz
return res;
}
理解后可以换成下面这种写法
void split(int p, int v, int &x, int &y) { // 这个引用写法类似将 x, y 返回后赋值
if (!p) { // 当前节点为空,分裂出的两棵树也都为空
x = 0;
y = 0;
return;
}
if (tr[p].v <= v) {
x = p; // 将 x 改为 p
split(tr[x].s[1], v, tr[x].s[1], y); // 递归右子树,并将右儿子改为右子树的分裂出的左边的树的根
} else {
y = p; // 将 y 改为 p
split(tr[y].s[0], v, x, tr[y].s[0]); // 递归左子树,并将左儿子改为左子树的分裂出的右边的树的根
}
pushup(p); // 更新 p 的 sz
}
模拟一下,现在要将下面这棵树按 v = 5 分开(当前根用 rt 表示,最终分裂出的根用 x_0, y_0 表示,这里节点的编号与值相等)。
首先递归 p = 4,x 为 x_0,y 为 y_0。

现在 $p = 6$,$x$ 为 $rs(4)$,$y$ 为 $y_0$。
$p$ 的值为 $6 > v = 5$,将 $y$ 设为 $6$,向左儿子递归。

现在 $p = 5$,$x$ 为 $rs(4)$,$y$ 为 $ls(6)$。
$p$ 的值为 $5 \le v = 5$,将 $x$ 设为 $5$,向右儿子递归。(这里修改 $rs(4)$ 为 $5$ 是加边并删边)

现在 $p = 0$,$x$ 为 $rs(5)$,$y$ 为 $ls(6)$。
现在到了空节点,将 $x, y$ 设为 $0$。(这里修改 $ls(6)$ 为 $0$ 是删了边)

回溯时更新完子树大小。
最后的树就是这样:

---
接下来是合并操作。
现在有以 $x, y$ 为根的左右两棵树,左边的树的任意值 $\le$ 右边的树的任意值,要按随机权值合并成一棵树,根为 $rt$。
若 $x$ 的权值 $\le y$ 的权值,那么说明 $y$ 应该在 $x$ 的右子树,且 $x$ 应为树的根,向 $x$ 的右儿子递归,将 $x$ 的右儿子改为递归出的根。
若 $x$ 的权值 $> y$ 的权值,那么说明 $x$ 应该在 $y$ 的左子树,且 $x$ 应为树的根,向 $y$ 的左儿子递归,将 $y$ 的左儿子改为递归出的根。
可以发现,这样合并出的树的边一定是初始时的边或者是 $x \to y$ 和 $x \gets y$,所以满足二叉搜索树的性质。
最后更新一下根的子树大小即可。
```cpp line-numbers
int merge(int x, int y) {
if (!x || !y) { // 若一个为空,返回另一个
return x + y;
}
int p;
if (tr[x].k < tr[y].k) {
p = x; // 将 p 改成 x
tr[x].s[1] = merge(tr[x].s[1], y); // 向右儿子递归并修改右儿子
} else {
p = y; // 将 p 改成 y
tr[y].s[0] = merge(x, tr[y].s[0]); // 向左儿子递归并修改左儿子
}
pushup(p); // 更新 p 的 sz
return p; // 返回根
}
```
模拟一下,现在要将下面这两棵树合并(根用 $x_0, y_0$ 表示,最终合并出的根用 $rt$ 表示,这里节点的编号与值相等)。

首先递归 $x = 4, y = 6$。
$x$ 的随机权值 $\le y$ 的随机权值,将 $p$ 设为 $x = 4$, 向 $x$ 的右儿子递归。

现在 $x = 5, y = 6$。
$x$ 的随机权值 $> y$ 的随机权值,将 $p$ 设为 $y = 4$,向 $y$ 的左儿子递归。(这里修改 $rs(4)$ 为 $6$ 是加边并删边)

现在 $x = 5, y = 0$。
$y = 0$,将 $p$ 设为 $x = 5$,终止递归。(这里修改 $rs(4)$ 为 $6$ 是加了边)

回溯时更新完子树大小。
最后的树就是这样:

---
接下来是插入和删除操作。
现在要插入一个值为 $v$ 的点,考虑将树分成两部分,一部分 $x$ 的值 $\le v$,另一部分 $y$ 的值 $> v$,新建一个值为 $v$ 的点 $z$。
此时值的大小关系为 $x \le z < y$,先合并 $x,z$,再合并 $xz, y$,并将 $rt$ 修改。
```cpp line-numbers
void ins(int v) {
int x, y, z;
split(rt, v, x, y); // 分裂出 x, y
z = add(v); // 新建一个节点 z
rt = merge(merge(x, z), y); // 合并并更新 rt
}
```
要在下面图中插入一个值为 $5$ 的点(随机权值取 $8$)。

分裂。

合并。

再合并。

现在要删除一个值为 $v$ 的点,考虑将树先分成两部分,一部分 $xz$ 的值 $\le v$,另一部分 $y$ 的值 $> v$,再将 $xz$ 分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $z$ 的值 $> v - 1$ 且 $\le v$ 即 $= v$。
此时直接将 $z$ 的左右儿子合并,即删掉根,删掉一个 $= v$ 的节点。
此时值的大小关系为 $x < z < y$,先合并 $x,z$,再合并 $xz, y$,并将 $rt$ 修改。
```cpp line-numbers
void del(int v) {
int x, y, z;
split(rt, v, x, y);
split(x, v - 1, x, z);
z = merge(tr[z].s[0], tr[z].s[1]);
rt = merge(merge(x, z), y);
}
```
要在下面图中删除一个值为 $5$ 的点。

分裂。

再分裂。

将 $z$ 左右儿子合并(删除根)。

合并。

再合并。

---
接下来是一个辅助操作,查询 $p$ 中第 $k$ 小的点的编号。
直接判断是不是当前点,是则返回,否则判断在左还是在右,向儿子递归。
```cpp line-numbers
int getk(int p, int k) {
if (k == tr[tr[p].s[0]].sz + 1) {
return p;
}
if (k <= tr[tr[p].s[0]].sz) {
return getk(tr[p].s[0], k);
} else {
return getk(tr[p].s[1], k - tr[tr[p].s[0]].sz - 1);
}
}
```
---
接下来是找前驱/后继。
现在找 $v$ 的前驱,考虑将树先分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $y$ 的值 $> v - 1$ 即 $\ge v$。
直接找到 $x$ 中第 $sz$ 小的(最大的),输出值即可。
```cpp line-numbers
void getpre(int v) {
int x, y;
split(rt, v - 1, x, y);
int p = getk(x, tr[x].sz);
cout << tr[p].v << endl;
rt = merge(x, y);
}
```
现在找 $v$ 的后继,考虑将树先分成两部分,一部分 $x$ 的值 $\le v$,另一部分 $y$ 的值 $> v$。
直接找到 $y$ 中第 $1$ 小的(最小的),输出值即可。
```cpp line-numbers
void getsuc(int v) {
int x, y;
split(rt, v, x, y);
int p = getk(y, 1);
cout << tr[p].v << endl;
rt = merge(x, y);
}
```
接下来是查排名。
现在查 $v$ 的前驱,考虑将树先分成两部分,一部分 $x$ 的值 $\le v - 1$ 即 $< v$,另一部分 $y$ 的值 $> v - 1$ 即 $\ge v$。
$x$ 的 $sz$ 即为 $< v$ 的个数,$+1$ 为排名。
```cpp line-numbers
void getrank(int v) {
int x, y;
split(rt, v - 1, x, y);
cout << tr[x].sz + 1 << endl;
rt = merge(x, y);
}
```
接下来是查第 $k$ 小值。
找到第 $k$ 小输出值即可。
```cpp line-numbers
void getv(int k) {
int p = getk(rt, k);
cout << tr[p].v << endl;
}
```
不难发现上面所有的操作单次复杂度不超过 $\mathcal O(h)$($h$ 为树高)。而随机化让树保持平衡, $h\sim\log n$。
:::success[AC code]
```cpp line-numbers
// Problem: P3369 【模板】普通平衡树
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3369
// Memory Limit: 128 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
const int N = 1e5 + 7;
int q;
struct Node {
int s[2];
int v;
int k;
int sz;
} tr[N];
int rt;
int idx;
int add(int v) {
tr[++idx].v = v;
tr[idx].k = rand();
tr[idx].sz = 1;
return idx;
}
void pushup(int x) {
tr[x].sz = tr[tr[x].s[0]].sz + tr[tr[x].s[1]].sz + 1;
}
void split(int p, int v, int & x, int & y) {
if (!p) {
x = 0;
y = 0;
return;
}
if (tr[p].v <= v) {
x = p;
split(tr[x].s[1], v, tr[x].s[1], y);
} else {
y = p;
split(tr[y].s[0], v, x, tr[y].s[0]);
}
pushup(p);
}
int merge(int x, int y) {
if (!x || !y) {
return x + y;
}
int p;
if (tr[x].k < tr[y].k) {
p = x;
tr[x].s[1] = merge(tr[x].s[1], y);
} else {
p = y;
tr[y].s[0] = merge(x, tr[y].s[0]);
}
pushup(p);
return p;
}
void ins(int v) {
int x, y, z;
split(rt, v, x, y);
z = add(v);
rt = merge(merge(x, z), y);
}
void del(int v) {
int x, y, z;
split(rt, v, x, y);
split(x, v - 1, x, z);
z = merge(tr[z].s[0], tr[z].s[1]);
rt = merge(merge(x, z), y);
}
int getk(int p, int k) {
if (k == tr[tr[p].s[0]].sz + 1) {
return p;
}
if (k <= tr[tr[p].s[0]].sz) {
return getk(tr[p].s[0], k);
} else {
return getk(tr[p].s[1], k - tr[tr[p].s[0]].sz - 1);
}
}
void getpre(int v) {
int x, y;
split(rt, v - 1, x, y);
int p = getk(x, tr[x].sz);
cout << tr[p].v << endl;
rt = merge(x, y);
}
void getsuc(int v) {
int x, y;
split(rt, v, x, y);
int p = getk(y, 1);
cout << tr[p].v << endl;
rt = merge(x, y);
}
void getrank(int v) {
int x, y;
split(rt, v - 1, x, y);
cout << tr[x].sz + 1 << endl;
rt = merge(x, y);
}
void getv(int k) {
int p = getk(rt, k);
cout << tr[p].v << endl;
}
signed main() {
cin >> q;
while (q--) {
int op, x;
cin >> op >> x;
if (op == 1) {
ins(x);
}
if (op == 2) {
del(x);
}
if (op == 3) {
getrank(x);
}
if (op == 4) {
getv(x);
}
if (op == 5) {
getpre(x);
}
if (op == 6) {
getsuc(x);
}
}
return 0;
}
```
:::