平衡树

平衡树

对于二叉搜索树而言,常见的平衡性定义是指:以 T 为根的树,每一个左子树和右子树的高度差最多为 1。

Treap

概述

Treap 是一弱平衡二叉搜索树。他同时符合二叉搜索树的性质,名字也因此为 tree(树) 和 heap(堆)的结合。

一般情况下,我们会赋予一个节点两个值,一个值用作二叉搜索树,一个值用作堆。其中,前者为节点编号(记为val),后者为随机数(记为 priority)。

下图就是一个 Treap 的例子(这里使用的是小根堆)。

oiwiki.org/ds/images/treap-treap-example.svg

Treap 又分为 有旋 Treap 和 无旋 Treap(FHQ_Treap)。

先来说有旋 Treap(简称 Treap)。

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

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

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

因此有如下定义:

  1. i 的左儿子变成根节点,成为右旋。
  2. i 的右儿子变为根节点,成为左旋。

oi-wiki.org/ds/images/treap-rotate.svg

然后我问的所有人都告诉我这玩意儿除了快没多大用,那就到此为止,啥时候想学了再来学。

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;
}

分裂

分裂操作和两个参数有关,根节点 i 和关键值 key

分裂操作分为按值分类和按排名分类两种,这里以按值分类为例。

分裂操作就是将一棵 Treap 按权值裁成小于等于 key 或者大于 key 的两棵 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;
	}
}

插入

假设要插入的数是 x,那么我们按照 x 讲 Treap 分裂成 a,b 两部分,将 xa 合并,再与 b 合并即可。

split(root, k, x, y); 
create(now, k);
root = merge(merge(x, now), y);

删除

我们考虑先将小于等于 x 的部分与大于 x 的部分分离。对于第一部分,我们再将小于 x 的部分和等于 x 的部分分离,最后中间删除等于 x 的部分即可。

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 【模板】普通平衡树

// 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;
}

维护区间

一般来讲,平衡树用于维护权值,线段树用于维护区间。但既然线段树有权值线段树,那么平衡树自然也有区间平衡树。

建树

区间平衡树需要按照小标建树。我们直接将新加入的点与原先的数合并 即可。

建树完后,树的中序遍历为数组。

分裂

上面我们提到过,分裂的方式有两种:按值分裂和按排名分裂。现在维护区间的平衡树就要按排名分裂。

或者说,我们叫它按大小分裂。我们将 k 个点放在分裂出来的一个 Treap 中,剩下的放在另一个 Treap 中。那么通过比较该节点 size 就可以判断哪个子树。

区间翻转

首先我们发现,翻转一段区间在平衡树上的操作其实就是翻转每个点的左右儿子。

我们将整棵树按 r 分裂成两棵树,再将左边那棵树按 l1 分裂。中间的那棵树就代表 [l,r]。我们翻转中间的树即可。

但是我们发现直接翻转是肯定不行的,所以我们就可以拿出懒标记。用懒标记记录是否要交换左右儿子,如果是就 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;
}

树套树

树套树有很多种,比如线段树套平衡树、线段树套权值树状数组、平衡树套平衡树……在这里只讨论一种线段树套平衡树

例题:P3380 【模板】树套树

思路

树套树就是在树的每个节点上开一棵树。

线段树套平衡树就是在线段树的每一个节点上维护一颗平衡树,这样就可以实现“区间平衡树”的效果。

代码实现

代码又臭又长,但是没有什么难理解的地方,直接放代码。

#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;
}

  1. 二叉搜索树的性质是:左子结点的值比父亲,右子节点的值比父亲↩︎

  2. 堆的性质是:子节点比父亲大(小根堆)或比父亲小(大根堆)。 ↩︎

posted @   Zctf1088  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示