平衡树

1 二叉搜索树

1.1 定义

二叉搜索树(Binary Sort Tree,BST)是一种二叉树的树形数据结构,定义如下:

  1. 空树是一颗二叉搜索树。
  2. 若二叉搜索树的左子树不为空,则其左子树上的所有点的权值都小于根节点的值。
  3. 若二叉搜索树的右子树不为空,则其右子树上的所有点的权值都大于根节点的值。
  4. 二叉搜索树的左右儿子都是二叉搜索树。

1.2 特性

在一般情况下,二叉搜索树插入和查询复杂度为 \(O(\log n)\)。但在一些特殊的情况下,例如插入序列单增时,二叉搜索树会被卡到近似一条链。此时其复杂度会退化到 \(O(n)\)

我们有一条性质:当二叉搜索树深度最小时,二叉搜索树的最高复杂度最低。

因此我们要在保留二叉搜索树的特性同时,使其深度尽可能小。这种维护二叉搜索树“平衡”的数据结构,就是平衡树。本文将介绍 Treap,Splay 以及 WBLT 这三种平衡树。

2 Treap

2.1 概述

Treap = Tree + Heap。顾名思义,就是 BST 和堆组合而成的数据结构。相比较与其他平衡树而言,Treap 实现起来较为简单。

2.1.1 Treap 的性质

  1. Treap 是一颗完全二叉树,且 Treap 上每一个点有权值和优先级。其中优先级在加点中被随机赋予。
  2. Treap 上每一个点的左右儿子的优先级均不大于或不小于当前点的优先级(满足堆的性质)。

2.2 有旋 Treap

2.2.1 旋转

有旋 Treap 使用旋转来维护平衡。

考虑二叉搜索树的这样一个性质:在只考虑 \(i,j\) 两点的情况下,\(i\)\(j\) 的左儿子等价于 \(j\)\(i\) 的右儿子。反之亦然。

我们假设要交换两点 \(i,j\),假设 \(j\)\(i\) 的左儿子,那么我们要将 \(i\) 变为 \(j\) 的右儿子。由于 \(i\) 顶替了 \(j\) 的右儿子的位置,所以让 \(j\) 的右儿子变成 \(i\)​ 的左儿子。

因此有如下定义:

  1. 将节点 \(i\) 的左儿子变为根节点,称为右旋。
  2. 将节点 \(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\) 的父亲。

  1. \(zig\):当 \(p\) 已经是根节点,即 \(x\)\(p\) 的儿子时进行。此时直接将 \(x\) 进行对应旋转即可。
  2. \(zig-zig\):当 \(p\) 不为根节点,且 \(p\) 与父亲的相对位置和 \(x\) 与父亲的相对位置相同时进行。此时先旋转 \(p\) ,然后旋转 \(x\)
  3. \(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;
}
posted @ 2024-02-27 17:53  UKE_Automation  阅读(123)  评论(0)    收藏  举报