平衡树
平衡树
对于二叉搜索树[1]而言,常见的平衡性定义是指:以 T 为根的树,每一个左子树和右子树的高度差最多为 1。
Treap
概述
Treap 是一弱平衡的二叉搜索树。他同时符合二叉搜索树和堆[2]的性质,名字也因此为 tree(树) 和 heap(堆)的结合。
一般情况下,我们会赋予一个节点两个值,一个值用作二叉搜索树,一个值用作堆。其中,前者为节点编号(记为
下图就是一个 Treap 的例子(这里使用的是小根堆)。
Treap 又分为 有旋 Treap 和 无旋 Treap(FHQ_Treap)。
先来说有旋 Treap(简称 Treap)。
有旋 Treap 使用旋转来维护平衡。
考虑二叉搜索树的这样一个性质:在只考虑
我们假设要交换两点
因此有如下定义:
- 将
的左儿子变成根节点,成为右旋。 - 将
的右儿子变为根节点,成为左旋。
然后我问的所有人都告诉我这玩意儿除了快没多大用,那就到此为止,啥时候想学了再来学。
FHQ-Treap
概述
FHQ-Treap,又名无旋 Treap。
FHQ-Treap 不使用旋转操作来维护平衡,他利用分裂和合并两个操作维护平衡。
基础操作
定义结构体
放个代码
mt19937 wdz(time(0)); // 随机数
const int N = 1e5 + 10;
int tot, root;
struct FHQ_Treap {
int l, r, val, key, siz; // 左儿子,右儿子,用于二叉搜索树的值,用于堆的值,子树大小
} tr[N];
#define lc tr[p].l
#define rc tr[p].r
void update(int p) { // 更新 p 的子树大小
tr[p].siz = tr[lc].siz + tr[rc].siz + 1;
}
void create(int &p, int x) { // 新建一棵只有一个节点的 Treap
p = ++tot;
tr[p].val = x;
tr[p].key = wdz();
tr[p].siz = 1;
}
分裂
分裂操作和两个参数有关,根节点
分裂操作分为按值分类和按排名分类两种,这里以按值分类为例。
分裂操作就是将一棵 Treap 按权值裁成小于等于
放上代码:
void split(int p, int k, int &x, int &y) {
// 根节点,关键值,以及分裂后的两个节点
if (!p) {
x = y = 0;
return;
}
if (tr[p].val <= k) { // 权值小于等于 k
x = p; // 左子树全部属于第一个子树
split(rc, k, rc, y); // 分裂右子树
} else {
y = p;
split(lc, k, x, lc);
}
update(p);
}
合并
合并操作就是将两棵 Treap 合并成一棵 Treap。
由于此时两棵 Treap 中,一棵绝对严格小于另一棵。因此我们此时只需要维护堆的性质即可。
因此关键在于将谁作为谁的什么子树。
反复递归即可(和线段树合并的代码还是很像的)。
int merge(int x, int y) { // 返回合并后的树根
if (!x || !y) {
return x + y;
}
if (tr[x].key < tr[y].key) { // x 的优先级小于 y 的优先级
tr[x].r = merge(tr[x].r, y);
// 将子树 y 并入子树 x 的右子树
update(x);
return x;
} else {
tr[y].l = merge(x, tr[y].l);
update(y);
return y;
}
}
插入
假设要插入的数是
split(root, k, x, y);
create(now, k);
root = merge(merge(x, now), y);
删除
我们考虑先将小于等于
split(root, k, x, tmp);
split(x, k - 1, x, y);
// 分离子树
y = merge(tr[y].l, tr[y].r);
// 合并 x 的子树,也就是去掉 x
root = merge(merge(x, y), tmp);
查询
查询就是查询排名为几的数。
int kth(int p, int k) { // 查询在 p 及其子树中排名为 k 的数
if (k == tr[lc].siz + 1) { // 为当前节点
return tr[p].val;
}
if (k <= tr[lc].siz) { // 在左子树中
return kth(lc, k);
} else { // 在右子树中
return kth(rc, k - tr[lc].siz - 1);
}
}
代码实现
// P3369【模板】普通平衡树
#include <bits/stdc++.h>
using namespace std;
mt19937 wdz(time(0)); // 随机数
const int N = 1e5 + 10;
int tot, root;
struct FHQ_Treap {
int l, r, val, key, siz;
} tr[N];
#define lc tr[p].l
#define rc tr[p].r
void update(int p) { // 更新 p 的子树大小
tr[p].siz = tr[lc].siz + tr[rc].siz + 1;
}
void create(int &p, int x) { // 新建一棵只有一个节点的 Treap
p = ++tot;
tr[p].val = x;
tr[p].key = wdz();
tr[p].siz = 1;
}
void split(int p, int k, int &x, int &y) {
// 根节点,关键值,以及分裂后的两个节点
if (!p) {
x = y = 0;
return;
}
if (tr[p].val <= k) { // 权值小于等于 k
x = p; // 左子树全部属于第一个子树
split(rc, k, rc, y); // 分裂右子树
} else {
y = p;
split(lc, k, x, lc);
}
update(p);
}
int merge(int x, int y) { // 返回合并后的树根
if (!x || !y) {
return x + y;
}
if (tr[x].key < tr[y].key) { // x 的优先级小于 y 的优先级
tr[x].r = merge(tr[x].r, y);
// 将子树 y 并入子树 x 的右子树
update(x);
return x;
} else {
tr[y].l = merge(x, tr[y].l);
update(y);
return y;
}
}
int kth(int p, int k) { // 查询在 p 及其子树中排名为 k 的数
if (k == tr[lc].siz + 1) { // 为当前节点
return tr[p].val;
}
if (k <= tr[lc].siz) { // 在左子树中
return kth(lc, k);
} else { // 在右子树中
return kth(rc, k - tr[lc].siz - 1);
}
}
int qq;
int now, tmp, x, y;
int main() {
scanf("%d", &qq);
while (qq--) {
int op, k;
scanf("%d%d", &op, &k);
if (op == 1) {
// 插入 k
split(root, k, x, y);
create(now, k);
root = merge(merge(x, now), y);
} else if (op == 2) {
// 删除 k
split(root, k, x, tmp);
split(x, k - 1, x, y);
// 分离子树
y = merge(tr[y].l, tr[y].r);
// 合并 x 的子树,也就是去掉 x
root = merge(merge(x, y), tmp);
} else if (op == 3) {
// 查询 k 数的排名
split(root, k - 1, x, y); // 分离子树
printf("%d\n", tr[x].siz + 1); // 节点数即为排名
root = merge(x, y);
} else if (op == 4) {
// 查询排名为 k 的数
printf("%d\n", kth(root, k));
} else if (op == 5) {
// 前驱
split(root, k - 1, x, y);
printf("%d\n", kth(x, tr[x].siz));
root = merge(x, y);
} else {
// 后继
split(root, k, x, y);
printf("%d\n", kth(y, 1));
root = merge(x, y);
}
}
return 0;
}
维护区间
一般来讲,平衡树用于维护权值,线段树用于维护区间。但既然线段树有权值线段树,那么平衡树自然也有区间平衡树。
建树
区间平衡树需要按照小标建树。我们直接将新加入的点与原先的数合并 即可。
建树完后,树的中序遍历为数组。
分裂
上面我们提到过,分裂的方式有两种:按值分裂和按排名分裂。现在维护区间的平衡树就要按排名分裂。
或者说,我们叫它按大小分裂。我们将 size
就可以判断哪个子树。
区间翻转
首先我们发现,翻转一段区间在平衡树上的操作其实就是翻转每个点的左右儿子。
我们将整棵树按
但是我们发现直接翻转是肯定不行的,所以我们就可以拿出懒标记。用懒标记记录是否要交换左右儿子,如果是就 pushdown
即可。
最后只要当经过节点的时候下放标记即可。
区间操作
其余的各种区间操作同样可以利用区间平衡树解决,例如区间加、区间乘、区间最值、区间覆盖等。只要维护对应的懒标记即可 。
代码实现
// P4146 序列终结者
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read() {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
const int N = 5e4 + 10;
int n, m;
mt19937 wdz(time(0));
struct node {
int l, r, val, key, siz, mx, lazy, t;
} tr[N];
#define lc tr[p].l
#define rc tr[p].r
int root, tot;
void create(int &p, int val) {
tr[p = ++tot] = {0, 0, val, wdz(), 1};
tr[p].mx = val;
tr[p].lazy = tr[p].t = 0;
}
void pushup(int p) {
tr[p].siz = tr[lc].siz + tr[rc].siz + 1;
tr[p].mx = tr[p].val;
if (lc) tr[p].mx = max(tr[p].mx, tr[lc].mx);
if (rc) tr[p].mx = max(tr[p].mx, tr[rc].mx);
}
void pushdown(int p) {
if (tr[p].lazy) {
if (lc) {
tr[lc].lazy += tr[p].lazy;
tr[lc].val += tr[p].lazy;
tr[lc].mx += tr[p].lazy;
}
if (rc) {
tr[rc].lazy += tr[p].lazy;
tr[rc].val += tr[p].lazy;
tr[rc].mx += tr[p].lazy;
}
tr[p].lazy = 0;
pushup(p);
}
if (tr[p].t) {
if (lc) tr[lc].t ^= 1;
if (rc) tr[rc].t ^= 1;
swap(lc, rc);
tr[p].t = 0;
}
}
void split(int p, int k, int &x, int &y) {
if (!p) {
x = y = 0;
return;
}
pushdown(p);
if (k <= tr[lc].siz) {
y = p;
split(lc, k, x, lc);
} else {
x = p;
split(rc, k - tr[lc].siz - 1, rc, y);
}
pushup(p);
}
int merge(int x, int y) {
if (!x || !y) {
return x + y;
}
if (tr[x].key < tr[y].key) {
pushdown(x);
tr[x].r = merge(tr[x].r, y);
pushup(x);
return x;
} else {
pushdown(y);
tr[y].l = merge(x, tr[y].l);
pushup(y);
return y;
}
}
void add(int l, int r, int v) {
int x, y, z;
split(root, r, y, z);
split(y, l - 1, x, y);
tr[y].val += v;
tr[y].lazy += v;
tr[y].mx += v;
root = merge(merge(x, y), z);
}
void change(int l, int r) {
int x, y, z;
split(root, r, y, z);
split(y, l - 1, x, y);
tr[y].t ^= 1;
root = merge(merge(x, y), z);
}
int query(int l, int r) {
int x, y, z;
split(root, r, y, z);
split(y, l - 1, x, y);
int ans = tr[y].mx;
root = merge(merge(x, y), z);
return ans;
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) {
int tmp; create(tmp, 0);
root = merge(root, tmp);
}
while (m--) {
int op = read();
if (op == 1) {
int l = read(), r = read(), v = read();
add(l, r, v);
} else if (op == 2) {
int l = read(), r = read();
change(l, r);
} else {
int l = read(), r = read();
printf("%lld\n", query(l, r));
}
}
return 0;
}
树套树
树套树有很多种,比如线段树套平衡树、线段树套权值树状数组、平衡树套平衡树……在这里只讨论一种线段树套平衡树
思路
树套树就是在树的每个节点上开一棵树。
线段树套平衡树就是在线段树的每一个节点上维护一颗平衡树,这样就可以实现“区间平衡树”的效果。
代码实现
代码又臭又长,但是没有什么难理解的地方,直接放代码。
#include <bits/stdc++.h>
using namespace std;
int read() {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
const int INF = (1 << 31) - 1;
const int N = 5e4 + 10;
int n, m, a[N];
mt19937 wdz(time(0));
struct PingHengShu {
int tot;
struct FHQ_Treap {
int l, r, val, key, siz;
} tr[N * 40];
#define lc tr[p].l
#define rc tr[p].r
void update(int p) {
tr[p].siz = tr[lc].siz + tr[rc].siz + 1;
}
void create(int &p, int x) {
p = ++tot;
tr[p].val = x;
tr[p].key = wdz();
tr[p].siz = 1;
}
void split(int p, int k, int &x, int &y) {
if (!p) {
x = y = 0;
return;
}
if (tr[p].val <= k) {
x = p;
split(rc, k, rc, y);
} else {
y = p;
split(lc, k, x, lc);
}
update(p);
}
int merge(int x, int y) {
if (!x || !y) return x + y;
if (tr[x].key < tr[y].key) {
tr[x].r = merge(tr[x].r, y);
update(x);
return x;
} else {
tr[y].l = merge(x, tr[y].l);
update(y);
return y;
}
}
int kth(int p, int k) {
if (tr[lc].siz + 1 == k) {
return tr[p].val;
}
if (k <= tr[lc].siz) {
return kth(lc, k);
} else {
return kth(rc, k - tr[lc].siz - 1);
}
}
int rnk(int &root, int k) {
int x, y, ans;
split(root, k - 1, x, y);
ans = tr[x].siz;
root = merge(x, y);
return ans;
}
int pre(int &root, int k) {
int x, y, ans;
split(root, k - 1, x, y);
if (tr[x].siz) ans = kth(x, tr[x].siz);
else ans = -INF;
root = merge(x, y);
return ans;
}
int nxt(int &root, int k) {
int x, y, ans;
split(root, k, x, y);
if (tr[y].siz) ans = kth(y, 1);
else ans = INF;
root = merge(x, y);
return ans;
}
void del(int &root, int k) {
int x, y, z;
split(root, k - 1, x, y);
split(y, k, y, z);
y = merge(tr[y].l, tr[y].r);
root = merge(merge(x, y), z);
}
void ins(int &root, int k) {
int x, y, z;
if (!root) {
create(root, k);
return;
}
split(root, k, x, y);
create(z, k);
root = merge(merge(x, z), y);
}
#undef lc
#undef rc
} fhq;
struct XianDuanShu {
struct node {
int l, r, root;
} tree[4 * N];
#define lc p << 1
#define rc p << 1 | 1
void build(int p, int l, int r) {
tree[p].l = l, tree[p].r = r;
for (int i = l; i <= r; i++) {
fhq.ins(tree[p].root, a[i]);
}
if (l == r) return;
int mid = l + r >> 1;
build(lc, l, mid);
build(rc, mid + 1, r);
}
int rnk(int p, int l, int r, int x) {
if (tree[p].l == l && tree[p].r == r) {
return fhq.rnk(tree[p].root, x);
}
int mid = tree[p].l + tree[p].r >> 1;
if (r <= mid) {
return rnk(lc, l, r, x);
} else if (l > mid) {
return rnk(rc, l, r, x);
} else {
return rnk(lc, l, mid, x) + rnk(rc, mid + 1, r, x);
}
}
int kth(int l, int r, int k) {
int ll = 0, rr = 1e8, mid;
while(ll < rr) {
mid = (ll + rr + 1) >> 1;
int p = rnk(1, l, r, mid);
if (p < k) {
ll = mid;
} else {
rr = mid - 1;
}
}
return rr;
}
void update(int p, int x, int v) {
fhq.del(tree[p].root, a[x]);
fhq.ins(tree[p].root, v);
if (tree[p].l == tree[p].r) return;
int mid = tree[p].l + tree[p].r >> 1;
if (x <= mid) {
update(lc, x, v);
} else {
update(rc, x, v);
}
}
int pre(int p, int l, int r, int x) {
if (tree[p].l == l && tree[p].r == r) {
return fhq.pre(tree[p].root, x);
}
int mid = tree[p].l + tree[p].r >> 1;
if (r <= mid) {
return pre(lc, l, r, x);
} else if (l > mid) {
return pre(rc, l, r, x);
} else {
return max(pre(lc, l, mid, x), pre(rc, mid + 1, r, x));
}
}
int nxt(int p, int l, int r, int x) {
if (tree[p].l == l && tree[p].r == r) {
return fhq.nxt(tree[p].root, x);
}
int mid = tree[p].l + tree[p].r >> 1;
if (r <= mid) {
return nxt(lc, l, r, x);
} else if (l > mid) {
return nxt(rc, l, r, x);
} else {
return min(nxt(lc, l, mid, x), nxt(rc, mid + 1, r, x));
}
}
#undef lc
#undef rc
} seg;
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
}
seg.build(1, 1, n);
while (m--) {
int op = read();
if (op == 1) {
int l = read(), r = read(), x = read();
printf("%d\n", seg.rnk(1, l, r, x) + 1);
} else if (op == 2) {
int l = read(), r = read(), k = read();
printf("%d\n", seg.kth(l, r, k));
} else if (op == 3) {
int p = read(), x = read();
seg.update(1, p, x);
a[p] = x;
} else if (op == 4) {
int l = read(), r = read(), x = read();
printf("%d\n", seg.pre(1, l, r, x));
} else {
int l = read(), r = read(), x = read();
printf("%d\n", seg.nxt(1, l, r, x));
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】