平衡树
1 二叉搜索树
1.1 定义
二叉搜索树(Binary Sort Tree,BST)是一种二叉树的树形数据结构,定义如下:
- 空树是一颗二叉搜索树。
- 若二叉搜索树的左子树不为空,则其左子树上的所有点的权值都小于根节点的值。
- 若二叉搜索树的右子树不为空,则其右子树上的所有点的权值都大于根节点的值。
- 二叉搜索树的左右儿子都是二叉搜索树。
1.2 特性
在一般情况下,二叉搜索树插入和查询复杂度为 \(O(\log n)\)。但在一些特殊的情况下,例如插入序列单增时,二叉搜索树会被卡到近似一条链。此时其复杂度会退化到 \(O(n)\)。
我们有一条性质:当二叉搜索树深度最小时,二叉搜索树的最高复杂度最低。
因此我们要在保留二叉搜索树的特性同时,使其深度尽可能小。这种维护二叉搜索树“平衡”的数据结构,就是平衡树。本文将介绍 Treap,Splay 以及 WBLT 这三种平衡树。
2 Treap
2.1 概述
Treap = Tree + Heap。顾名思义,就是 BST 和堆组合而成的数据结构。相比较与其他平衡树而言,Treap 实现起来较为简单。
2.1.1 Treap 的性质
- Treap 是一颗完全二叉树,且 Treap 上每一个点有权值和优先级。其中优先级在加点中被随机赋予。
- Treap 上每一个点的左右儿子的优先级均不大于或不小于当前点的优先级(满足堆的性质)。
2.2 有旋 Treap
2.2.1 旋转
有旋 Treap 使用旋转来维护平衡。
考虑二叉搜索树的这样一个性质:在只考虑 \(i,j\) 两点的情况下,\(i\) 为 \(j\) 的左儿子等价于 \(j\) 为 \(i\) 的右儿子。反之亦然。
我们假设要交换两点 \(i,j\),假设 \(j\) 为 \(i\) 的左儿子,那么我们要将 \(i\) 变为 \(j\) 的右儿子。由于 \(i\) 顶替了 \(j\) 的右儿子的位置,所以让 \(j\) 的右儿子变成 \(i\) 的左儿子。
因此有如下定义:
- 将节点 \(i\) 的左儿子变为根节点,称为右旋。
- 将节点 \(i\) 的右儿子变为根节点,称为左旋。
因此我们可以用旋转操作维护堆的性质。我们指定优先级满足小根堆性质。
代码如下:
首先定义结构体,需要维护的信息是左右儿子、权值、当前权值出现的次数、优先级、子树节点个数。
struct Treap {
int son[2], val, cnt, key, size;
}t[Maxn];
接下来完成旋转操作,在旋转完后维护子树节点个数。
void update(int p) {//更新子树节点个数
t[p].size = t[lp].size + t[rp].size + t[p].cnt;
}
void rotate(int &p, int d) {//d为方向,1为左旋,0 为右旋
int tmp = t[p].son[d ^ 1];
t[p].son[d ^ 1] = t[tmp].son[d];
t[tmp].son[d] = p;
update(p);
p = tmp;//更新当前根节点
}
2.2.2 插入
当我们插入一个节点,如果这个点的优先级小于父亲的优先级,就要交换他和父亲。利用旋转交换即可。
void insert(int &p, int x) {
if(!p) {//如果有可以直接放入的空位
p = ++tot;
t[p].size = t[p].cnt = 1;
t[p].val = x;
t[p].key = rand();
return ;
}
if(t[p].val == x) {//已有当前节点
t[p].cnt++;
t[p].size++;
return ;
}
int d = (x > t[p].val);
insert(t[p].son[d], x);
if(t[p].key > t[t[p].son[d]].key) {
rotate(p, d ^ 1);
}
update(p);
}
2.2.3 删除
我们考虑用堆的方法删除。我们将要删除的点与他优先级较小的点交换,直到其变为叶子结点,就直接删除该点。
void del(int &p, int x) {
if(!p) return;//没有该节点
if(x < t[p].val) {
del(lp, x)//查左子树
}
else if(x > t[p].val) {
del(rp, x);//查右子树
}
else {//已经找到
if(!lp && !rp) {//叶子结点
t[p].cnt--;
t[p].size--;
if(t[p].cnt == 0) {
p = 0;
}
}
else if(lp && !rp) {//左子树不空
rotate(p, 1)//左旋
del(rp, x);
}
else if(!lp && rp) {//右子树不空
rotate(p, 0);//右旋
del(lp, x);
}
else {
int d = (t[lp].key < t[rp].ley);
rotate(p, d);//向优先级高的旋
del(t[p].son[d], x);
}
}
update(p);
}
2.2.4 查询排名
直接计算该子树中小于 val 的节点个数 + 1。
int ask(int p, int x) {
if(!p) return 1;//空节点
if(t[p].val == x) {//当前点权值等于 x
return t[lp].size + 1;
}
else if(t[p].val < x) {//当前点权值小于 x
return ask(rp, x) + t[lp].size + t[p].cnt;
}
else {//当前点圈住大于 x
return ask(lp, x);
}
}
2.2.5 查询值
只需要判断出当前排名在树的哪个部分即可,类似于权值线段树。
int find(int p, int x) {
if(!p) return 0;//空节点
if(t[lp].size >= x) {//当前点排名大于 x
return find(lp, x);
}
else if(t[lp].size + t[p].cnt < x) {//当前点排名小于 x
return find(rp, x - t[lp].size - t[p].cnt);
}
else {//当前点排名就是 x
return t[p].val;
}
}
2.2.6 求前驱
利用二叉搜索树的性质求即可。
int pre(int p, int x) {
if(!p) return INT_MIN;//空节点
if(t[p].val >= x) {//权值大于等于 x
return pre(lp, x);//搜索左子树
}
else {
return max(t[p].val, pre(rp, x));
}
}
2.2.7 求后继
同上。
int nxt(int p, int x) {
if(!p) return INT_MAX;//空节点
if(t[p].val <= x) {//权值小于等于 x
return nxt(rp, x);//搜索左子树
}
else {
return min(t[p].val, nxt(lp, x));
}
}
2.2.8 完整代码
P3369 【模板】普通平衡树 AC 代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
struct Treap {
int son[2], val, cnt, key, size;
}t[Maxn];
#define lp t[p].son[0]
#define rp t[p].son[1]
int tot, root;//树的总节点数,用于传参的一个变量(同线段树合并)
void update(int p) {//更新子树节点个数
t[p].size = t[lp].size + t[rp].size + t[p].cnt;
}
void rotate(int &p, int d) {//d为方向,1为左旋,0 为右旋
int tmp = t[p].son[d ^ 1];
t[p].son[d ^ 1] = t[tmp].son[d];
t[tmp].son[d] = p;
update(p);
update(tmp);
p = tmp;//更新当前根节点
}
void insert(int &p, int x) {
if(!p) {//如果有可以直接放入的空位
p = ++tot;
t[p].size = t[p].cnt = 1;
t[p].val = x;
t[p].key = rand();
return ;
}
if(t[p].val == x) {//已有当前节点
t[p].cnt++;
t[p].size++;
return ;
}
int d = (x > t[p].val);
insert(t[p].son[d], x);
if(t[p].key > t[t[p].son[d]].key) {//不满足堆性质
rotate(p, d ^ 1);//旋转
}
update(p);
}
void del(int &p, int x) {
if(!p) return;//没有该节点
if(x < t[p].val) {
del(lp, x);//查左子树
}
else if(x > t[p].val) {
del(rp, x);//查右子树
}
else {//已经找到
if(!lp && !rp) {//叶子结点
t[p].cnt--;
t[p].size--;
if(t[p].cnt == 0) {
p = 0;
}
}
else if(lp && !rp) {//左子树不空
rotate(p, 1);//左旋
del(rp, x);
}
else if(!lp && rp) {//右子树不空
rotate(p, 0);//右旋
del(lp, x);
}
else {
int d = (t[lp].key < t[rp].key);
rotate(p, d);//向优先级高的旋
del(t[p].son[d], x);
}
}
update(p);
}
int ask(int p, int x) {
if(!p) return 1;//空节点
if(t[p].val == x) {//当前点权值等于 x
return t[lp].size + 1;
}
else if(t[p].val < x) {//当前点权值小于 x
return ask(rp, x) + t[lp].size + t[p].cnt;
}
else {//当前点圈住大于 x
return ask(lp, x);
}
}
int find(int p, int x) {
if(!p) return 0;//空节点
if(t[lp].size >= x) {//当前点排名大于 x
return find(lp, x);
}
else if(t[lp].size + t[p].cnt < x) {//当前点排名小于 x
return find(rp, x - t[lp].size - t[p].cnt);
}
else {//当前点排名就是 x
return t[p].val;
}
}
int pre(int p, int x) {
if(!p) return INT_MIN;//空节点
if(t[p].val >= x) {//权值大于等于 x
return pre(lp, x);//搜索左子树
}
else {
return max(t[p].val, pre(rp, x));
}
}
int nxt(int p, int x) {
if(!p) return INT_MAX;//空节点
if(t[p].val <= x) {//权值小于等于 x
return nxt(rp, x);//搜索左子树
}
else {
return min(t[p].val, nxt(lp, x));
}
}
int n;
int main() {
ios::sync_with_stdio(0);
srand(time(0));
cin >> n;
while(n--) {
int opt, x;
cin >> opt >> x;
switch(opt) {
case 1: {
insert(root, x);
break;
}
case 2: {
del(root, x);
break;
}
case 3: {
cout << ask(root, x) << '\n';
break;
}
case 4: {
cout << find(root, x) << '\n';
break;
}
case 5: {
cout << pre(root, x) << '\n';
break;
}
case 6: {
cout << nxt(root, x) << '\n';
break;
}
}
}
return 0;
}
3 FHQ-Treap
3.1 概述
上文中我们介绍了使用旋转维护 Treap 的方式,事实上另一种更常用的方式是采用合并与分裂来维护平衡。这种写法一般称之为 FHQ-Treap,也叫无旋 Treap。
这种平衡树的优点是容易理解且容易实现,是现在最流行的一种平衡树,缺点是常数较大。
3.2 分裂
分裂操作和两个参数有关,根节点 \(i\) 和关键值 \(key\)。
分裂操作分为按值分类和按排名分类两种,这里以按值分类为例。
分裂操作就是将一颗 Treap 按权值裁成小于等于 \(key\) 或者大于 \(key\) 的两颗 Treap。
重复递归分裂即可。
下面看代码。首先定义结构体:
struct FHQ_Treap {
int l, r, val, key, size;
}t[Maxn];
接下来进行分裂操作:
void update(int p) {//更新子树节点数
t[p].size = t[lp].size + t[rp].size + 1;
}
void split(int p, int k, int &x, int &y) {
//根节点,关键值,以及分裂后两个子树的根
if(!p) {
x = y = 0;
return ;
}
if(t[p].val <= k) {//权值小于等于 k
x = p;//左子树全部属于第一个子树
split(rp, k, rp, y); //分裂右子树
}
else {//权值大于 x
y = p;//右子树全部属于第二个子树
split(lp, k, x, lp);//分裂左子树
}
update(p);
}
3.3 合并
合并就是将两颗 Treap 合并成一颗 Treap。
由于此时两颗 Treap 中,一颗绝对严格小于另一颗。因此我们此时只需要维护堆的性质即可。
(在有旋 Treap 中,用旋转操作维护堆的性质。而在 FHQ-Treap 中,我们用合并操作维护堆的性质)
因此关键在于将谁作为谁的什么子树。
反复递归即可(其实和线段树合并的代码很像)。
int merge(int x, int y) {//返回合并后树根节点
if(!x || !y) {
return x + y;
}
if(t[x].key < t[y].key) {//x 的优先级小于 y 的优先级
t[x].r = merge(t[x].r, y);
//将子树 y 并入子树 x 的右子树
update(x);
return x;
}
else {//x 的优先级大于 y 的优先级
t[y].l = merge(x, t[y].l);
//将子树 x 并入子树 y 的左子树
update(y);
return y;
}
}
3.4 插入
假设要插入的数是 \(x\),那么我们按 \(x\) 将 Treap 分裂成 \(a,b\) 两部分,将 \(x\) 与 \(a\) 合并,再与 \(b\) 合并即可。
3.5 删除
我们考虑先将小于等于 \(x\) 的部分与大于 \(x\) 的部分分离。对于第一部分,我们再将小于 \(x\) 和等于 \(x\) 的部分分离,最后中间等于 \(x\) 的部分删除即可。
3.6 查询
显然查询 \(a\) 的排名与在普通 BST 里没有什么区别。
至于求值、求前驱后继,与上面的分裂合并思想是一致的。
下面直接看完整代码。
3.7 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
struct FHQ_Treap {
int l, r, val, key, size;
}t[Maxn];
int tot, root;
//节点数,传参变量
#define lp t[p].l
#define rp t[p].r
void update(int p) {//更新子树节点数
t[p].size = t[lp].size + t[rp].size + 1;
}
void create(int &p, int x) {
p = ++tot;
t[p].val = x;
t[p].key = rand();
t[p].size = 1;
}
void split(int p, int k, int &x, int &y) {
//根节点,关键值,以及分裂后两个子树的根
if(!p) {
x = y = 0;
return ;
}
if(t[p].val <= k) {//权值小于等于 k
x = p;//左子树全部属于第一个子树
split(rp, k, rp, y); //分裂右子树
}
else {//权值大于 x
y = p;//右子树全部属于第二个子树
split(lp, k, x, lp);//分裂左子树
}
update(p);
}
int merge(int x, int y) {//返回合并后树根节点
if(!x || !y) {
return x + y;
}
if(t[x].key < t[y].key) {//x 的优先级小于 y 的优先级
t[x].r = merge(t[x].r, y);
//将子树 y 并入子树 x 的右子树
update(x);
return x;
}
else {//x 的优先级大于 y 的优先级
t[y].l = merge(x, t[y].l);
//将子树 x 并入子树 y 的左子树
update(y);
return y;
}
}
int kth(int p, int k) {
if(k == t[lp].size + 1) {//为当前点
return t[p].val;
}
if(k <= t[lp].size) {//在左子树中
return kth(lp, k);
}
else {//在右子树中
return kth(rp, k - t[lp].size - 1);
}
}
int n;
int tmp;
int main() {
srand(time(0));
ios::sync_with_stdio(0);
cin >> n;
int now, x, y;//当前节点,分裂后树根
while(n--) {
int opt, k;
cin >> opt >> k;
switch(opt) {
case 1: {
split(root, k, x, y);
create(now, k);
root = merge(merge(x, now), y);
break;
}
case 2: {
split(root, k, x, tmp);
split(x, k - 1, x, y);
//分裂子树
y = merge(t[y].l, t[y].r);
//合并 x 的子树(也就是去掉 x)
root = merge(merge(x, y), tmp);
break;
}
case 3: {
split(root, k - 1, x, y);//分离子树
cout << t[x].size + 1 << '\n';//节点数量即为排名
root = merge(x, y);
break;
}
case 4: {
cout << kth(root, k) << '\n';
break;
}
case 5: {
split(root, k - 1, x, y);
cout << kth(x, t[x].size) << '\n';
//x 的前驱也就是排名在 x 前一位的数,节点数量为 size
root = merge(x, y);
break;
}
case 6: {
split(root, k, x, y);
cout << kth(y, 1) << '\n';
//x 的后继也就是排名在 x 后一位的数,节点数量为 1
root = merge(x, y);
break;
}
}
}
return 0;
}
3.8 维护区间
一般来讲,平衡树用于维护权值,线段树用于维护区间。但既然线段树有权值线段树,那么平衡树自然也有区间平衡树。
3.8.1 建树
区间平衡树需要按下标建树。我们直接将新加入的点与原先的树合并即可。
建树完后,树的中序遍历为原数组。
3.8.2 分裂
上面我们提到过,分裂方式有两种:按值分裂和按排名分裂。现在维护区间的平衡树就要按排名分裂。
或者说,我们叫他按大小分裂。我们将 \(k\) 个点放在左树中,剩下的放在右树中。那么通过比较该节点 size 就可以判断分裂在那个子树。
3.8.3 区间翻转
首先我们容易发现(其实不容易),翻转一段区间在平衡树上的操作其实就是翻转每个点的左右儿子。
我们将整棵树按 \(r\) 分裂成两棵树,再将左边那棵树按 \(l-1\) 分裂。中间的那棵树就代表区间 \([l,r]\)。我们直接翻转中间的树即可。
但是我们发现,这样做的复杂度是假的。我们思考后发现,每一次翻转都不一定对之后的操作有影响。
因此,我们需要用到一个熟悉的东西——懒标记。
用懒标记记录是否要交换左右儿子,如果是就 pushdown 即可。
最后只要当经过节点的时候下放标记即可。
3.8.3 区间操作
其余的各种区间操作同样可以利用区间平衡树解决,例如区间加、区间乘、区间最值、区间平推等等。只需要维护对应的懒标记即可。
3.8.4 代码
P3391 【模板】文艺平衡树,区间翻转模板题。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
struct FHQ_Treap {
int l, r, val, siz, key, tag;
}t[Maxn];
int tot, root;
#define lp t[p].l
#define rp t[p].r
int create(int p) {///建立新节点
t[++tot] = {0, 0, p, 1, rand(), 0};
return tot;
}
void pushup(int p) {
t[p].siz = t[lp].siz + t[rp].siz + 1;
}
void pushdown(int p) {//下放懒标记
if(t[p].tag) {
swap(lp, rp);
t[lp].tag ^= 1;
t[rp].tag ^= 1;
t[p].tag = 0;
}
}
void split(int p, int k, int &x, int &y) {//分裂
if(!p) {
x = y = 0;
return ;
}
pushdown(p);
if(k <= t[lp].siz) {
y = p;
split(lp, k, x, lp);
}
else {
x = p;
split(rp, k - t[lp].siz - 1, rp, y);
}
pushup(p);
}
int merge(int x, int y) {//合并
if(!x || !y) {
return x + y;
}
if(t[x].key < t[y].key) {
pushdown(x);
t[x].r = merge(t[x].r, y);
pushup(x);
return x;
}
else {
pushdown(y);
t[y].l = merge(x, t[y].l);
pushup(y);
return y;
}
}
void print(int p) {//中序遍历输出
if(!p) return;
pushdown(p);
print(lp);
cout << t[p].val << " ";
print(rp);
}
int n, m;
int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {//下标建树
root = merge(root, create(i));
}
while(m--) {
int l, r;
cin >> l >> r;
int x, y, z;
split(root, r, x, z);
split(x, l - 1, x, y);//提取 [l,r] 区间
t[y].tag ^= 1;//标记
x = merge(x, y);
root = merge(x, z);//合并回去
}
print(root);
return 0;
}
(当然你也可以尝试使用平衡树去做线段树)
4 Splay
4.1 概述
Splay 树,又称伸展树,通过伸展操作不断将某个节点旋转至根节点,以此来维护平衡。在均摊 \(O(\log n)\) 的复杂度内完成插入、查找、删除操作。
4.2 基础 Splay
4.2.1 基本操作
首先定义 Splay 的结构体,与 Treap 的定义可以说是一模一样,但是多了一个父亲。
下面先实现三个操作。
pushup(p):同 Treap,更新节点的siz。get(p):判断节点 \(p\) 是父亲的左儿子和右儿子。clear(p):销毁节点 \(p\)。
struct Splay {
int fa, son[2], val, siz, cnt;
}t[Maxn];
#define lp (t[p].son[0])
#define rp (t[p].son[1])
int rt, tot;
void pushup(int p) {
t[p].siz = t[lp].siz + t[rp].siz + t[p].cnt;
}
bool get(int p) {
return p == t[t[p].fa].son[1];
}
void clear(int p) {
t[p] = {0, 0, 0, 0, 0, 0};
}
4.2.2 旋转操作
Splay 树的旋转操作与 Treap 树的旋转操作基本一样,分为左旋和右旋,在此不再赘述。
注意还是略有不同,这里我们旋转的就是这个节点,而不是他的儿子。
void rotate(int p) {//下列讲解以右旋为例
int y = t[p].fa, z = t[y].fa, d = get(p);
t[y].son[d] = t[p].son[d ^ 1]; // 将 y 的左儿子指向 p 的右儿子、
if(t[p].son[d ^ 1]) t[t[p].son[d ^ 1]].fa = y;//p 的右儿子的父亲指向 y
t[p].son[d ^ 1] = y;//将 p 的左儿子指向 y
t[y].fa = p;//y 的父亲指向 p
t[p].fa = z;//p 的父亲指向 z
if(z) t[z].son[y == t[z].son[1]] = p;//将 y 原本的位置给 p
pushup(y);
pushup(p);
}
4.2.3 Splay 操作
Splay 树要求我们每操作一个节点,就要让该节点旋转至根节点。
一个简单的方法是,通过不断左旋右旋来达成目的。这被称作单旋。
然而单旋很容易被卡,因此一般不考虑。
这时候就需要双旋了。双旋的操作分为三种,首先定义 \(x\) 节点为当前节点,\(p\) 为 \(x\) 的父亲,\(g\) 为 \(p\) 的父亲。
- \(zig\):当 \(p\) 已经是根节点,即 \(x\) 为 \(p\) 的儿子时进行。此时直接将 \(x\) 进行对应旋转即可。
- \(zig-zig\):当 \(p\) 不为根节点,且 \(p\) 与父亲的相对位置和 \(x\) 与父亲的相对位置相同时进行。此时先旋转 \(p\) ,然后旋转 \(x\)。
- \(zig-zag\):当 \(p\) 不为根节点,且 \(p\) 与父亲的相对位置和 \(x\) 与父亲的相对位置不同时进行。此时将 \(x\) 旋转两次即可。
接下来放几张图,分别对应三个过程:
代码如下:
void splay(int p) {
int f = t[p].fa;//父亲节点
while(f) {//不断旋转至根节点
if(t[f].fa) {
rotate(get(p) == get(f) ? f : p);
//zig-zig 和 zig-zag
//区别就是旋转父亲还是当前节点
}
rotate(p);
//无论如何都要旋转当前节点
f = t[p].fa;
}
rt = p;
}
4.2.4 插入
基本维护的操作结束后,就是其他操作了。
首先插入不是很难,与 Treap 类似,只需要注意进行 Splay 操作即可。
void insert(int k) {
if(!rt) {//空树
t[++tot].val = k;//直接新建节点
t[tot].cnt++;
rt = tot;
pushup(rt);
return ;
}
int p = rt, f = 0;
while(1) {//模拟递归
if(t[p].val == k) {//已有当前节点
t[p].cnt++;
pushup(p), pushup(f);
splay(p);
break;
}
f = p;
p = t[p].son[t[p].val < k];//模拟递归查找过程
//如果当前值小于 k 向右儿子查,否则向左儿子
if(!p) {//找到且没有出现
t[++tot].val = k;
t[tot].cnt++;
t[tot].fa = f;
t[f].son[t[f].val < k] = tot;//新建节点
pushup(tot), pushup(f);
splay(tot);
break;
}
}
}
4.2.5 查询排名
显然直接按照定义查询即可。
int rnk(int k) {
int res = 0, p = rt;
while(1) {
if(k < t[p].val) {//向左子树寻找
p = lp;
}
else {//向右子树寻找
res += t[lp].siz;//累加答案
if(k == t[p].val) {//找到位置
splay(p);
return res + 1;
}
res += t[p].cnt;//注意累加当前节点次数
if(!rp) {
if(p) splay(p);
return res + 1;
}
p = rp;
}
}
}
4.2.6 查询值
依然按照定义查询。
int kth(int k) {
int p = rt;
while(1) {
if(lp && k <= t[lp].siz) {//在左子树
p = lp;
}
else {//在右子树
k -= (t[lp].siz + t[p].cnt);//减掉左边的排名
if(k <= 0) {//在当前节点
splay(p);
return t[p].val;
}
p = rp;
}
}
}
4.2.7 求前驱
首先我们插入 \(x\),此时 \(x\) 就是根节点,然后他的前驱就是左子树中最靠右的节点。因此直接在左子树中不断找右儿子即可。
int pre() {
int p = t[rt].son[0];//根节点(x)的左子树
if(!p) return p;
while(rp) p = rp;//不断找右儿子
splay(p);
return p;
}
4.2.8 求后继
与上面类似,为 \(x\) 的右子树中最靠左的节点。
int nxt() {
int p = t[rt].son[1];//根节点(x)的右子树
if(!p) return p;
while(lp) p = lp;//不断找左儿子
splay(p);
return p;
}
4.2.9 删除
Splay 的删除是基于合并的。我们设两棵树为 \(x,y\)(满足 \(x\) 每一个值小于 \(y\) 中的值),如果要合并两棵树,那么我们先将 \(x\) 树中的最大值旋到根节点,然后将 \(y\) 接到根节点的右子树即可。
有了上面的前置,现在我们来看如何删除 Splay 的节点。
首先将 \(x\) 旋转到根节点,然后看 \(cnt\) 的数量。
- 如果有不止一个 \(x\),那么将数量减一即可。
- 否则合并两颗左右子树即可。
代码如下:
void del(int k) {
rnk(k);//随便搞一个操作让 k 旋转到根节点
if(t[rt].cnt > 1) {
t[rt].cnt--;
pushup(rt);
return;
}
if(!t[rt].son[0] && !t[rt].son[1]) {//如果只有一个节点
clear(rt);//删除,变为空树
rt = 0;
return ;
}
if(!t[rt].son[0]) {//只有右子树
int cur = rt;
rt = t[rt].son[1];//根为右子树的根
t[rt].fa = 0;
clear(cur);
return ;
}
if(!t[rt].son[1]) {//只有左子树
int cur = rt;
rt = t[rt].son[0];
t[rt].fa = 0;//根为左子树的根
clear(cur);
return ;
}
//都有,需要合并
int cur = rt, x = pre();//此时根节点的前驱即为左子树中最大的数
t[t[cur].son[1]].fa = x;
t[x].son[1] = t[cur].son[1]; //右子树挂到左子树上
clear(cur);
pushup(rt);
return ;
}
4.2.10 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
struct Splay {
int fa, son[2], val, siz, cnt;
}t[Maxn];
#define lp (t[p].son[0])
#define rp (t[p].son[1])
int rt, tot;
void pushup(int p) {
t[p].siz = t[lp].siz + t[rp].siz + t[p].cnt;
}
bool get(int p) {
return p == t[t[p].fa].son[1];
}
void clear(int p) {
t[p] = {0, 0, 0, 0, 0, 0};
}
void rotate(int p) {//下列讲解以右旋为例
int y = t[p].fa, z = t[y].fa, d = get(p);
t[y].son[d] = t[p].son[d ^ 1]; // 将 y 的左儿子指向 p 的右儿子、
if(t[p].son[d ^ 1]) t[t[p].son[d ^ 1]].fa = y;//p 的右儿子的父亲指向 y
t[p].son[d ^ 1] = y;//将 p 的左儿子指向 y
t[y].fa = p;//y 的父亲指向 p
t[p].fa = z;//p 的父亲指向 z
if(z) t[z].son[y == t[z].son[1]] = p;//将 y 原本的位置给 p
pushup(y);
pushup(p);
}
void splay(int p) {
int f = t[p].fa;//父亲节点
while(f) {//不断旋转至根节点
if(t[f].fa) {
rotate(get(p) == get(f) ? f : p);
//zig-zig 和 zig-zag
//区别就是旋转父亲还是当前节点
}
rotate(p);
//无论如何都要旋转当前节点
f = t[p].fa;
}
rt = p;
}
void insert(int k) {
if(!rt) {//空树
t[++tot].val = k;//直接新建节点
t[tot].cnt++;
rt = tot;
pushup(rt);
return ;
}
int p = rt, f = 0;
while(1) {//模拟递归
if(t[p].val == k) {//已有当前节点
t[p].cnt++;
pushup(p), pushup(f);
splay(p);
break;
}
f = p;
p = t[p].son[t[p].val < k];//模拟递归查找过程
//如果当前值小于 k 向右儿子查,否则向左儿子
if(!p) {//找到且没有出现
t[++tot].val = k;
t[tot].cnt++;
t[tot].fa = f;
t[f].son[t[f].val < k] = tot;//新建节点
pushup(tot), pushup(f);
splay(tot);
break;
}
}
}
int rnk(int k) {
int res = 0, p = rt;
while(1) {
if(k < t[p].val) {//向左子树寻找
p = lp;
}
else {//向右子树寻找
res += t[lp].siz;//累加答案
if(k == t[p].val) {//找到位置
splay(p);
return res + 1;
}
res += t[p].cnt;//注意累加当前节点次数
if(!rp) {
if(p) splay(p);
return res + 1;
}
p = rp;
}
}
}
int kth(int k) {
int p = rt;
while(1) {
if(lp && k <= t[lp].siz) {//在左子树
p = lp;
}
else {//在右子树
k -= (t[lp].siz + t[p].cnt);//减掉左边的排名
if(k <= 0) {//在当前节点
splay(p);
return t[p].val;
}
p = rp;
}
}
}
int pre() {
int p = t[rt].son[0];//根节点(x)的左子树
if(!p) return p;
while(rp) p = rp;//不断找右儿子
splay(p);
return p;
}
int nxt() {
int p = t[rt].son[1];//根节点(x)的右子树
if(!p) return p;
while(lp) p = lp;//不断找左儿子
splay(p);
return p;
}
void del(int k) {
rnk(k);//随便搞一个操作让 k 旋转到根节点
if(t[rt].cnt > 1) {
t[rt].cnt--;
pushup(rt);
return;
}
if(!t[rt].son[0] && !t[rt].son[1]) {//如果只有一个节点
clear(rt);//删除,变为空树
rt = 0;
return ;
}
if(!t[rt].son[0]) {//只有右子树
int cur = rt;
rt = t[rt].son[1];//根为右子树的根
t[rt].fa = 0;
clear(cur);
return ;
}
if(!t[rt].son[1]) {//只有左子树
int cur = rt;
rt = t[rt].son[0];
t[rt].fa = 0;//根为左子树的根
clear(cur);
return ;
}
//都有,需要合并
int cur = rt, x = pre();//此时根节点的前驱即为左子树中最大的数
t[t[cur].son[1]].fa = x;
t[x].son[1] = t[cur].son[1]; //右子树挂到左子树上
clear(cur);
pushup(rt);
return ;
}
int n;
int main() {
ios::sync_with_stdio(0);
cin >> n;
while(n--) {
int opt, x;
cin >> opt >> x;
switch(opt) {
case 1: {
insert(x);
break;
}
case 2: {
del(x);
break;
}
case 3: {
cout << rnk(x) << '\n';
break;
}
case 4: {
cout << kth(x) << '\n';
break;
}
case 5: {
insert(x);
cout << t[pre()].val << '\n';
del(x);
break;
}
case 6: {
insert(x);
cout << t[nxt()].val << '\n';
del(x);
break;
}
}
}
return 0;
}
4.3 维护区间
同 FHQ-Treap,Splay 也可以用于维护区间。
4.3.1 建树
我们模仿线段树的建树方式,递归建立区间 Splay 树。
int build(int l, int r, int f) {//返回编号
if(l > r) return 0;
int mid = (l + r) >> 1, p = ++tot;
t[p].val = a[mid], t[p].fa = f;//当前节点(为了满足中序遍历)
lp = build(l, mid - 1, p);
rp = build(mid + 1, r, p);//递归建树
pushup(p);
return p;
}
4.3.2 Splay 操作进阶
首先魔改一下 Splay 操作,我们加上一个值 v,表示要将 \(p\) 旋转至 \(v\) 的子树(\(v=0\) 时表示旋转至根节点)。
如下:
void splay(int p, int v) {
int f = t[p].fa;
while(f != v) { //区别只在于将 !=0 改成 != v
if(t[f].fa != v) {
rotate(get(p) == get(f) ? f : p);
}
rotate(p);
f = t[p].fa;
}
if(v == 0) rt = p;
}
4.3.3 区间翻转
假如翻转区间 \([l,r]\),我们先将 \(l-1\) 翻到根节点:

然后将 \(r+1\) 翻到 \(l-1\) 的下面:

这样 \(r+1\) 的左子树就是区间 \([l,r]\)。
同时注意一个细节,由于我们可能会翻转区间 \([1,n]\),因此实际建树时应当建 \([0,n+1]\),那么此时查找 \(l-1,r+1\) 的位置就是查询排名为 \(l,r+2\) 的数。
同时利用懒标记维护即可,不再赘述。
4.3.4 代码
把各种函数魔改一下就行了。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int Maxn = 2e5 + 5;
struct Splay {
int fa, son[2], val, siz, tag;
}t[Maxn];
#define lp (t[p].son[0])
#define rp (t[p].son[1])
int rt, tot;
void pushup(int p) {
t[p].siz = t[lp].siz + t[rp].siz + 1;
}
bool get(int p) {
return p == t[t[p].fa].son[1];
}
void clear(int p) {
t[p] = {0, 0, 0, 0, 0, 0};
}
void rotate(int p) {
int y = t[p].fa, z = t[y].fa, d = get(p);
t[y].son[d] = t[p].son[d ^ 1];
if(t[p].son[d ^ 1]) t[t[p].son[d ^ 1]].fa = y;
t[p].son[d ^ 1] = y;
t[y].fa = p;
t[p].fa = z;
if(z) t[z].son[y == t[z].son[1]] = p;
pushup(y);
pushup(p);
}
void splay(int p, int v) {
int f = t[p].fa;
while(f != v) {
if(t[f].fa != v) {
rotate(get(p) == get(f) ? f : p);
}
rotate(p);
f = t[p].fa;
}
if(v == 0) rt = p;
}
int build(int l, int r, int f) {//返回编号
if(l > r) return 0;
int mid = (l + r) >> 1, p = ++tot;
t[p].val = mid, t[p].fa = f;//当前节点(为了满足中序遍历)
lp = build(l, mid - 1, p);
rp = build(mid + 1, r, p);//递归建树
pushup(p);
return p;
}
void pushdown(int p) {
if(t[p].tag) {
swap(lp, rp);
t[lp].tag ^= 1;
t[rp].tag ^= 1;
t[p].tag = 0;
}
}
int kth(int k) {
int p = rt;
while(1) {
pushdown(p);
if(lp && k <= t[lp].siz) {//在左子树
p = lp;
}
else {//在右子树
k -= (t[lp].siz + 1);//减掉左边的排名
if(k <= 0) {//在当前节点
splay(p, 0);
return p;//注意返回的是节点编号而非权值
}
p = rp;
}
}
}
void reverse(int l, int r) {
int x = kth(l), y = kth(r + 2);
splay(x, 0), splay(y, x);
int p = t[t[rt].son[1]].son[0];
t[p].tag ^= 1;
}
int n, m;
void print(int p) {
pushdown(p);
if(lp) print(lp);
if(t[p].val != 0 && t[p].val != n + 1) {
cout << t[p].val << " ";
}
if(rp) print(rp);
}
int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
rt = build(0, n + 1, 0);
while(m--) {
int l, r;
cin >> l >> r;
reverse(l, r);
}
print(rt);
return 0;
}
5 WBLT
5.1 概述
WBLT,即 Weight Balanced Leafy Tree,是重量平衡树 WBT 和 Leafy Tree 的结合。它的优点是代码不长且常数很小,并且可以持久化。缺点是空间需要开 \(2\) 倍,但是实际上由于一些原因导致 WBLT 的空间不会有明显劣势。
5.2 Leafy Tree
Leafy Tree 的意思是将数据结构维护的信息全部放到叶子上维护,例如线段树就是一种 Leafy Tree。与之对应的 Nodey,也就是信息存在节点上,上面的所有平衡树都是 Nodey 的。
对于一棵 Leafy Tree,其叶子节点记录递增的数列,非叶子节点维护子树中叶子上的数的最大值,其节点数为 \(2n-1\),因此空间需要开 \(2\) 倍。同时除了叶子节点外,每个节点都一定有两个儿子。
根据 Leafy Tree 的性质,可以轻松实现二叉搜索树的基本操作。
5.2.1 查排名
从根节点开始往下走,累加左子树的大小。注意在 Leafy Tree 中我们定义一个子树的大小为子树中叶子的个数。
il int rnk(int k) {
int p = rt, cnt = 0;
while(1) {
if(t[p].siz == 1) return cnt + 1;
else if(k <= t[ls(p)].val) p = ls(p);
else cnt += t[ls(p)].siz, p = rs(p);
}
}
5.2.2 查第 k 小
同理根据左子树的大小决定往哪个方向走。
il int kth(int k) {
int p = rt;
while(1) {
if(t[p].siz == 1) return t[p].val;
else if(k <= t[ls(p)].siz) p = ls(p);
else k -= t[ls(p)].siz, p = rs(p);
}
}
5.2.3 查前驱后继
和上面平衡树一样,可以用前两种操作来实现查前驱后继。
il int pre(int k) {return kth(rnk(k) - 1);}
il int nxt(int k) {return kth(rnk(k + 1));}
5.2.4 插入
插入需要讲一下。首先按照查排名的方式往叶子节点走,走到一个叶子节点 \(p\) 之后我们需要新建两个点 \(x,y\),一个点的值是该叶子节点的值,另一个是我们要插入的值。然后将 \(p\) 的左右儿子设为 \(x,y\) 即可。注意要保证 \(x,y\) 的大小关系否则就错了。
il int newnode(int k) {t[++tot] = {0, 0, 1, k}; return tot;}
il void ins(int p, int k) {
if(t[p].siz == 1) {
int x = newnode(t[p].val), y = newnode(k);
if(t[x].val > t[y].val) swap(x, y);
ls(p) = x, rs(p) = y;
return pushup(p);
}
ins(k <= t[ls(p)].val ? ls(p) : rs(p), k);
}
5.2.5 删除
删除的时候依然先找到值对应的叶子 \(p\),然后删去这个叶子后 \(fa_p\) 就只有一个儿子了,所以我们需要将 \(fa_p\) 删掉并将 \(p\) 的兄弟提上去作为一个子树。实现的时候可以传引用来修改,同时我们可以在 \(fa_p\) 处就用那个叶子的兄弟来替换当前点。
il void del(int &p, int k) {
if(k <= t[ls(p)].val) {
if(t[ls(p)].siz == 1) p = rs(p);
else del(ls(p), k);
}
else {
if(t[rs(p)].siz == 1) p = ls(p);
else del(rs(p), k);
}
}
很显然这些操作的复杂度最坏是 \(O(n)\) 的。
5.3 维护重量平衡
上面操作复杂度爆炸很显然是因为没有维护平衡,那么 WBLT 维护平衡的方式来自于它名字的另一个来源:WBT 重量平衡树。
定义一个节点的重量 \(w_x=siz_x\),定义一个节点是 \(\alpha\) 平衡的当且仅当 \(\min\{w_{ls_x},w_{rs_x}\}\ge \alpha \cdot w_x\),其中 \(\alpha\) 是一个在 \((0,0.5]\) 的实数。如果一个子树的所有节点都是 \(\alpha\) 平衡的,就称这个子树是 \(\alpha\) 平衡的。
那么根据这个定义,一个含有 \(n\) 个元素的重量平衡树的树高 \(h\) 应当是 \(\log _{\frac{1}{1-\alpha}} n=O(\log n)\) 级别的,这样我们操作的复杂度就有保证了。而 WBLT 维护重量平衡的方式仍然有两种:旋转和合并分裂。这里介绍利用旋转进行维护的方式,这种方式常数较小。
显然我们需要维护平衡的地方只有插入删除,这些操作一次会影响一条链,我们依次处理这些节点。旋转的定义和上面的定义依然是类似的,这里直接给出代码:
il void rotate(int p, int d) {
swap(ls(p), rs(p));
swap(ls(son(p, d ^ 1)), rs(son(p, d ^ 1)));
swap(son(son(p, d ^ 1), d ^ 1), son(p, d));
pushup(son(p, d ^ 1)), pushup(p);
}
会发现这里做单旋操作时我们没有保证树的中序遍历不变,但是不要忘了 WBLT 的信息在叶子上,因此只要叶子的顺序不变就行。
但是只有单旋操作不行,因为还有一种情况需要进行双旋。这里直接给出结论:
- 设三个点 \(x,y,z\),\(y\) 为 \(x\) 的儿子且 \(z\) 为 \(y\) 的儿子且 \(y,z\) 方向不同。
- 若 \(\frac{w_z}{w_y}\ge \frac{1-2\alpha}{1-\alpha}\),则进行一次双旋,即将 \(z\) 转两次转到根。
- 否则我们只用转一次 \(y\) 即可。
具体可以看代码,想看证明可以去网上搜一下。
il void maintain(int p) {
if(t[p].siz == 1) return ;
int d;
if(t[ls(p)].siz < t[p].siz * alpha) d = 1;
else if(t[rs(p)].siz < t[p].siz * alpha) d = 0;
else return ;
if(t[son(son(p, d), d ^ 1)].siz * (1 - alpha) >= t[son(p, d)].siz * (1 - 2 * alpha))
rotate(son(p, d), d ^ 1);
rotate(p, d);
}
然后我们在插入删除的时候及时进行 maintain 操作维护平衡即可,复杂度 \(O(n\log n)\)。
模板题:【模板】普通平衡树,代码如下:
#include <bits/stdc++.h>
#define il inline
using namespace std;
const int N = 2e5 + 5;
const int Inf = 2e9;
template <typename T> il void chkmin(T &x, T y) {x = min(x, y);}
template <typename T> il void chkmax(T &x, T y) {x = max(x, y);}
bool Beg;
namespace My_Space {
struct IO {
static const int Size = (1 << 21);
char buf[Size], *p1, *p2; int st[105], Top;
~IO() {clear();}
il void clear() {fwrite(buf, 1, Top, stdout); Top = 0;}
il char gc() {return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, Size, stdin), p1 == p2) ? EOF : *p1++;}
il void pc(const char c) {Top == Size && (clear(), 0); buf[Top++] = c;}
il IO &operator >> (char &c) {while(c = gc(), c == ' ' || c == '\n' || c == '\r'); return *this;}
template <typename T> il IO &operator >> (T &x) {
x = 0; bool f = 0; char ch = gc();
while(!isdigit(ch)) {if(ch == '-') f = 1; ch = gc();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = gc();
f ? x = -x : 0; return *this;
}
il IO &operator << (const char c) {pc(c); return *this;}
template <typename T> il IO &operator << (T x) {
if(x < 0) pc('-'), x = -x;
do st[++st[0]] = x % 10, x /= 10; while(x);
while(st[0]) pc('0' + st[st[0]--]);
return *this;
}
il IO &operator << (const char *s) {for(int i = 0; s[i]; i++) pc(s[i]); return *this;}
}fin, fout;
int n, rt;
namespace WBLT {
const double alpha = 0.29;
struct node {
int ls, rs, siz, val;
}t[N << 1];
#define son(p, x) ((x) ? t[p].rs : t[p].ls)
#define ls(p) t[p].ls
#define rs(p) t[p].rs
int tot;
il int newnode(int k) {t[++tot] = {0, 0, 1, k}; return tot;}
il int rnk(int k) {
int p = rt, cnt = 0;
while(1) {
if(t[p].siz == 1) return cnt + 1;
else if(k <= t[ls(p)].val) p = ls(p);
else cnt += t[ls(p)].siz, p = rs(p);
}
}
il int kth(int k) {
int p = rt;
while(1) {
if(t[p].siz == 1) return t[p].val;
else if(k <= t[ls(p)].siz) p = ls(p);
else k -= t[ls(p)].siz, p = rs(p);
}
}
il int pre(int k) {return kth(rnk(k) - 1);}
il int nxt(int k) {return kth(rnk(k + 1));}
il void pushup(int p) {
t[p].siz = t[ls(p)].siz + t[rs(p)].siz;
t[p].val = t[rs(p)].val;
}
il void rotate(int p, int d) {
swap(ls(p), rs(p));
swap(ls(son(p, d ^ 1)), rs(son(p, d ^ 1)));
swap(son(son(p, d ^ 1), d ^ 1), son(p, d));
pushup(son(p, d ^ 1)), pushup(p);
}
il void maintain(int p) {
if(t[p].siz == 1) return ;
int d;
if(t[ls(p)].siz < t[p].siz * alpha) d = 1;
else if(t[rs(p)].siz < t[p].siz * alpha) d = 0;
else return ;
if(t[son(son(p, d), d ^ 1)].siz * (1 - alpha) >= t[son(p, d)].siz * (1 - 2 * alpha))
rotate(son(p, d), d ^ 1);
rotate(p, d);
}
il void ins(int p, int k) {
if(t[p].siz == 1) {
int x = newnode(t[p].val), y = newnode(k);
if(t[x].val > t[y].val) swap(x, y);
ls(p) = x, rs(p) = y;
return pushup(p);
}
ins(k <= t[ls(p)].val ? ls(p) : rs(p), k);
pushup(p); maintain(p);
}
il void del(int &p, int k) {
if(k <= t[ls(p)].val) {
if(t[ls(p)].siz == 1) p = rs(p);
else del(ls(p), k), pushup(p), maintain(p);
}
else {
if(t[rs(p)].siz == 1) p = ls(p);
else del(rs(p), k), pushup(p), maintain(p);
}
}
}
il void main() {
fin >> n; rt = WBLT::newnode(Inf);
while(n--) {
int opt, x; fin >> opt >> x;
switch(opt) {
case 1: {WBLT::ins(rt, x); break;}
case 2: {WBLT::del(rt, x); break;}
case 3: {fout << WBLT::rnk(x) << '\n'; break;}
case 4: {fout << WBLT::kth(x) << '\n'; break;}
case 5: {fout << WBLT::pre(x) << '\n'; break;}
case 6: {fout << WBLT::nxt(x) << '\n'; break;}
}
}
}
}
il void File() {freopen(".in", "r", stdin); freopen(".out", "w", stdout);}
bool End;
il void Usd() {cerr << (&Beg - &End) / 1024.0 / 1024.0 << "MB " << (double)clock() * 1000.0 / CLOCKS_PER_SEC << "ms\n";}
int main() {
My_Space::main();
Usd();
return 0;
}
5.4 维护区间
类似的,WBLT 也能用来维护区间,并且由于其是 Leafy Tree,和线段树的结构更加类似,因此实现区间操作的时候更加容易理解。
我们直接来看模板题:【模板】文艺平衡树。线段树难以维护的原因是因为要区间反转,而 WBLT 维护区间反转的方式与 FHQ 类似,采用分裂区间—打标记—合并区间的方式来解决。
5.4.1 区间懒标记
WBLT 的懒标记和正常标记是类似的,可以直接采用上面 FHQ 的实现。另外一种实现方式是标记当前的两个儿子需不需要进行交换,如果需要下放给儿子并交换儿子的两个子树。对应代码如下:
il void pushtag(int p) {t[p].tag ^= 1; swap(ls(p), rs(p));}
il void pushdown(int p) {
if(!t[p].tag) return ;
pushtag(ls(p)), pushtag(rs(p));
t[p].tag = 0;
}
5.4.2 合并
假设现在我们需要合并两个 \(\alpha\) 平衡的子树 \(x,y\),为了保证 \(\alpha\) 平衡的要求,我们需要对 \(x,y\) 的大小分类讨论一下。令 \(w=w_x+w_y\)。
- 如果 \(\min(w_x,w_y)\ge \alpha \cdot w\),那么直接新建一个节点 \(p\),把 \(p\) 的左右儿子设为 \(x,y\) 即可。
- 否则我们假设 \(w_x\ge w_y\)。
- 如果 \(w_{ls_x}\ge \alpha \cdot w\),那么我们将 \(rs_x\) 和 \(y\) 递归合并起来作为 \(x\) 新的右子树即可。
- 否则的话令 \(p=rs_x\),我们将 \(ls_x\) 和 \(ls_p\) 合并起来、\(rs_p\) 和 \(y\) 合并起来作为 \(x\) 的左右子树。然后此时 \(p\) 就是一个废节点,进行垃圾回收。
- \(w_x<w_y\) 同理。
直接递归实现即可,代码如下:
il int merge(int x, int y) {
if(!x || !y) return x + y;
int sum = t[x].siz + t[y].siz;
if(min(t[x].siz, t[y].siz) * 4 >= sum) {
int p = newnode(); ls(p) = x, rs(p) = y;
return pushup(p), p;
}
if(t[x].siz >= t[y].siz) {
pushdown(x);
if(t[ls(x)].siz * 4 >= sum) return rs(x) = merge(rs(x), y), pushup(x), x;
int p = rs(x); pushdown(p);
ls(x) = merge(ls(x), ls(p)), rs(x) = merge(rs(p), y);
ljt[++top] = p;
return pushup(x), x;
}
else {
pushdown(y);
if(t[rs(y)].siz * 4 >= sum) return ls(y) = merge(x, ls(y)), pushup(y), y;
int p = ls(y); pushdown(p);
rs(y) = merge(rs(p), rs(y)), ls(y) = merge(x, ls(p));
ljt[++top] = p;
return pushup(y), y;
}
}
需要注意的是 WBLT 合并的复杂度是 \(O(\log \frac{siz_x}{siz_y})\) 的,优于 FHQ 的 \(O(\log (siz_x+siz_y))\)。这在以后的学习中会有所作用。
5.4.3 分裂
分裂 WBLT 的时候需要注意,我们分裂的树也需要满足 \(\alpha\) 平衡,所以我们不能像 FHQ 一样直接连接左右儿子。在分裂的时候我们需要用上面的合并来进行连接操作,这样才能保证复杂度正确。
按排名分裂的代码如下:
il void split(int p, int k, int &x, int &y) {
if(!k) return x = 0, y = p, void();
if(t[p].siz == 1) return x = p, y = 0, void();
pushdown(p);
if(k <= t[ls(p)].siz) {
split(ls(p), k, x, y);
y = merge(y, rs(p)); ljt[++top] = p;
}
else {
split(rs(p), k - t[ls(p)].siz, x, y);
x = merge(ls(p), x); ljt[++top] = p;
}
}
5.4.4 代码
按照上面的思路我们就可以实现文艺平衡树了,建树的时候直接采用线段树的建树方式,这样一定是最平衡的。代码如下:
#include <bits/stdc++.h>
#define il inline
using namespace std;
const int N = 1e5 + 5;
const int Inf = 2e9;
template <typename T> il void chkmin(T &x, T y) {x = min(x, y);}
template <typename T> il void chkmax(T &x, T y) {x = max(x, y);}
bool Beg;
namespace My_Space {
struct IO {
static const int Size = (1 << 21);
char buf[Size], *p1, *p2; int st[105], Top;
~IO() {clear();}
il void clear() {fwrite(buf, 1, Top, stdout); Top = 0;}
il char gc() {return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, Size, stdin), p1 == p2) ? EOF : *p1++;}
il void pc(const char c) {Top == Size && (clear(), 0); buf[Top++] = c;}
il IO &operator >> (char &c) {while(c = gc(), c == ' ' || c == '\n' || c == '\r'); return *this;}
template <typename T> il IO &operator >> (T &x) {
x = 0; bool f = 0; char ch = gc();
while(!isdigit(ch)) {if(ch == '-') f = 1; ch = gc();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48), ch = gc();
f ? x = -x : 0; return *this;
}
il IO &operator << (const char c) {pc(c); return *this;}
template <typename T> il IO &operator << (T x) {
if(x < 0) pc('-'), x = -x;
do st[++st[0]] = x % 10, x /= 10; while(x);
while(st[0]) pc('0' + st[st[0]--]);
return *this;
}
il IO &operator << (const char *s) {for(int i = 0; s[i]; i++) pc(s[i]); return *this;}
}fin, fout;
int n, m;
int rt;
namespace WBLT {
struct node {
int ls, rs, val, siz, tag;
}t[N << 1];
#define ls(p) t[p].ls
#define rs(p) t[p].rs
#define son(p, x) ((x) ? t[p].rs : t[p].ls)
int ljt[N], top, tot;
il void pushup(int p) {t[p].siz = t[ls(p)].siz + t[rs(p)].siz;}
il void pushtag(int p) {t[p].tag ^= 1; swap(ls(p), rs(p));}
il void pushdown(int p) {
if(!t[p].tag) return ;
pushtag(ls(p)), pushtag(rs(p));
t[p].tag = 0;
}
il int newnode() {
int p = top ? ljt[top--] : ++tot;
t[p] = {0, 0, 0, 1, 0}; return p;
}
il void build(int &p, int l, int r) {
p = newnode();
if(l == r) return void(t[p].val = l);
int mid = (l + r) >> 1;
build(ls(p), l, mid), build(rs(p), mid + 1, r);
pushup(p);
}
il int merge(int x, int y) {
if(!x || !y) return x + y;
int sum = t[x].siz + t[y].siz;
if(min(t[x].siz, t[y].siz) * 4 >= sum) {
int p = newnode();
ls(p) = x, rs(p) = y;
return pushup(p), p;
}
if(t[x].siz >= t[y].siz) {
pushdown(x);
if(t[ls(x)].siz * 4 >= sum) return rs(x) = merge(rs(x), y), pushup(x), x;
int p = rs(x); pushdown(p);
ls(x) = merge(ls(x), ls(p)), rs(x) = merge(rs(p), y);
ljt[++top] = p;
return pushup(x), x;
}
else {
pushdown(y);
if(t[rs(y)].siz * 4 >= sum) return ls(y) = merge(x, ls(y)), pushup(y), y;
int p = ls(y); pushdown(p);
rs(y) = merge(rs(p), rs(y)), ls(y) = merge(x, ls(p));
ljt[++top] = p;
return pushup(y), y;
}
}
il void split(int p, int k, int &x, int &y) {
if(!k) return x = 0, y = p, void();
if(t[p].siz == 1) return x = p, y = 0, void();
pushdown(p);
if(k <= t[ls(p)].siz) {
split(ls(p), k, x, y);
y = merge(y, rs(p)); ljt[++top] = p;
}
else {
split(rs(p), k - t[ls(p)].siz, x, y);
x = merge(ls(p), x); ljt[++top] = p;
}
}
il void Reverse(int l, int r) {
int x, y, z;
split(rt, r, y, z); split(y, l - 1, x, y);
pushtag(y);
rt = merge(merge(x, y), z);
}
il void print(int p) {
if(t[p].siz == 1) return fout << t[p].val << ' ', void();
pushdown(p);
print(ls(p)), print(rs(p));
}
}
il void main() {
fin >> n >> m;
WBLT::build(rt, 1, n);
while(m--) {
int l, r; fin >> l >> r;
WBLT::Reverse(l, r);
}
WBLT::print(rt);
}
}
il void File() {freopen(".in", "r", stdin); freopen(".out", "w", stdout);}
bool End;
il void Usd() {cerr << (&Beg - &End) / 1024.0 / 1024.0 << "MB " << (double)clock() * 1000.0 / CLOCKS_PER_SEC << "ms\n";}
int main() {
My_Space::main();
Usd();
return 0;
}

浙公网安备 33010602011771号