平衡树之FHQ-treap

前置知识:二叉查找树

首先我们要看一下二叉查找树
它满足这些性质:
1.它是二叉树(废话)
2.对于任何一个根节点 它左子树的所有点都小于它 它右子树的所有点都大于它
所以实际上它的中序遍历就是对整个序列排序的结果

它非常的方便 支持查询很多的东西(后面会讲)
但是如果只是普通的插入 很容易让它复杂度假掉 所以我们需要一些手段来维持它的平衡
这就是平衡树


FHQ-treap

首先我们来看一下什么是 \(treap\)
实际上\(treap = tree + heap\) 非常的震撼啊我也没想到
具体是怎么回事呢
因为大根堆是一颗完全二叉树
我们给 \(BST\) 的每个节点都随机一个权值 然后拿这个权值把他们按大根堆排起来
那么这棵树就是期望平衡的
其中新建点和维护的操作如下:

inline void maintain(int k) {
	T[k].siz = T[T[k].l].siz + T[T[k].r].siz + 1;
}

int newnode(int val) {
	T[++tot].val = val;
	T[tot].siz = 1;
	T[tot].dat = rand();
	return tot;
}

\(FHQ-treap\) 没有 \(treap\) 的旋转操作 它非常的好学

它基于两个操作:分裂(split)和合并(merge)

  • split

我们把根为 \(k\) 的树按 \(key\) 分成 \(x y\) 两个部分
并且使 \(x\) 中的都小于等于 \(key\) \(y\) 中的都大于 \(key\)
代码如下:

void split(int k, int key, int &x, int &y) { //分裂的时候保证x中所有数都小于y中所有数 
	if (k == 0) {
		x = y = 0;
		return;
	}
	if (T[k].val <= key) { //键值大于val 就把这个点和它的左子树全给x 
		x = k;
		split(T[k].r, key, T[x].r, y); // 然后把它的右节点分成两半 一半给x的右子树一半给y 
	} else { // 反之亦然 
		y = k;
		split(T[k].l, key, x, T[y].l);
	}
	maintain(k);
}
  • merge

我们把 \(x y\) 两棵树合起来 并返回新的根节点
代码如下:

int merge(int x, int y) { //顺序不能反 理由同上 
	if (x == 0 || y == 0) return x + y;
	if (T[x].dat > T[y].dat) { //维护treap的大根堆性质 
		T[x].r = merge(T[x].r, y); //x的根节点应该在上面 所以把y合到x的右子树 
		maintain(x);
		return x;
	}
	else { //反之亦然 
		T[y].l = merge(x, T[y].l);
		maintain(y);
		return y;
	}
}

有了这两个操作 我们就可以干很多的事情:

  • insert
void insert(int key) { //插入键值为key的数 
	int x, y;
	split(root, key - 1, x, y); //把小于等于key-1的分给x 大于等于key的分给y 
	int id = newnode(key);
	root = merge(merge(x, id), y); //把新节点插入到x,y之间 
}
  • remove
void remove(int key) { //删除一个键值为key的数 
	int x, y, z;
	split(root, key - 1, x, y); //把小于等于key-1的分给x
	split(y, key, y, z); //把大于key的分给z 那y中就全是等于key的了
	if (y) y = merge(T[y].l, T[y].r); //因为只需要删除一个数 所以把根节点删掉左右俩儿子合并
	root = merge(merge(x, y), z); //然后重新合起来 如果要把权值为key的全删掉就把xz合起来 
}
  • rank
int rank(int key) { //查询key的排名 
	int x, y, ans;
	split(root, key - 1, x, y); //把小于key的数都分给x 那么siz[x]就是小于key的数的数量
	ans = T[x].siz + 1;
	root = merge(x, y); //掰完记得合起来 
	return ans; 
}
  • kth
int kth(int rnk) { //查询排名为rnk的数 
	int p = root;
	while (p) {
		if (T[T[p].l].siz + 1 == rnk) break; //正正好好查到
		else if (T[T[p].l].siz + 1 > rnk) p = T[p].l; //查过头了
		else {
			rnk -= T[T[p].l].siz + 1;
			p = T[p].r; //还不够 
		} 
	}
	return T[p].val;
}
  • get_pre
int get_pre(int key) { //查询key的前驱 
	int x, y, p;
	split(root, key - 1, x, y); //把比key小的都分给x 
	p = x;
	while (T[p].r != 0) p = T[p].r; //一直往大的方向走
	root = merge(x, y);
	return T[p].val; 
}
  • get_nxt
int get_nxt(int key) { //查询key的后缀 
	int x, y, p;
	split(root, key, x, y); //把比key大的都分给y
	p = y;
	while (T[p].l != 0) p = T[p].l;
	root = merge(x, y);
	return T[p].val; 
}

最后附上P3369 【模板】普通平衡树的代码:

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 0721;
int n;

struct tree {
	struct node {
		int l, r; //左儿子 右儿子 
		int val, siz, dat; //权值 大小 堆中的权值 
	} T[N];
	int tot = 0, root = 0;
	
	inline void maintain(int k) {
		T[k].siz = T[T[k].l].siz + T[T[k].r].siz + 1;
	}
	
	int newnode(int val) {
		T[++tot].val = val;
		T[tot].siz = 1;
		T[tot].dat = rand();
		return tot;
	}
	
	void split(int k, int key, int &x, int &y) { //分裂的时候保证x中所有数都小于y中所有数 
		if (k == 0) {
			x = y = 0;
			return;
		}
		if (T[k].val <= key) { //键值大于val 就把这个点和它的左子树全给x 
			x = k;
			split(T[k].r, key, T[x].r, y); // 然后把它的右节点分成两半 一半给x的右子树一半给y 
		} else { // 反之亦然 
			y = k;
			split(T[k].l, key, x, T[y].l);
		}
		maintain(k);
	}
	
	int merge(int x, int y) { //顺序不能反 理由同上 
		if (x == 0 || y == 0) return x + y;
		if (T[x].dat > T[y].dat) { //维护treap的大根堆性质 
			T[x].r = merge(T[x].r, y); //x的根节点应该在上面 所以把y合到x的右子树 
			maintain(x);
			return x;
		}
		else { //反之亦然 
			T[y].l = merge(x, T[y].l);
			maintain(y);
			return y;
		}
	}
	
	void insert(int key) { //插入键值为key的数 
		int x, y;
		split(root, key - 1, x, y); //把小于等于key-1的分给x 大于等于key的分给y 
		int id = newnode(key);
		root = merge(merge(x, id), y); //把新节点插入到x,y之间 
	}
	
	void remove(int key) { //删除一个键值为key的数 
		int x, y, z;
		split(root, key - 1, x, y); //把小于等于key-1的分给x
		split(y, key, y, z); //把大于key的分给z 那y中就全是等于key的了
		if (y) y = merge(T[y].l, T[y].r); //因为只需要删除一个数 所以把根节点删掉左右俩儿子合并
		root = merge(merge(x, y), z); //然后重新合起来 如果要把权值为key的全删掉就把xz合起来 
	}
	
	int rank(int key) { //查询key的排名 
		int x, y, ans;
		split(root, key - 1, x, y); //把小于key的数都分给x 那么siz[x]就是小于key的数的数量
		ans = T[x].siz + 1;
		root = merge(x, y); //掰完记得合起来 
		return ans; 
	}
	
	int kth(int rnk) { //查询排名为rnk的数 
		int p = root;
		while (p) {
			if (T[T[p].l].siz + 1 == rnk) break; //正正好好查到
			else if (T[T[p].l].siz + 1 > rnk) p = T[p].l; //查过头了
			else {
				rnk -= T[T[p].l].siz + 1;
				p = T[p].r; //还不够 
			} 
		}
		return T[p].val;
	}
	
	int get_pre(int key) { //查询key的前驱 
		int x, y, p;
		split(root, key - 1, x, y); //把比key小的都分给x 
		p = x;
		while (T[p].r != 0) p = T[p].r; //一直往大的方向走
		root = merge(x, y);
		return T[p].val; 
	}
	
	int get_nxt(int key) { //查询key的后缀 
		int x, y, p;
		split(root, key, x, y); //把比key大的都分给y
		p = y;
		while (T[p].l != 0) p = T[p].l;
		root = merge(x, y);
		return T[p].val; 
	}
} treap;

int main() {
	srand(time(0));
	
	scanf("%d", &n);
	while (n--) {
		int opt, x;
		scanf("%d%d", &opt, &x);
		if (opt == 1) treap.insert(x);
		else if (opt == 2) treap.remove(x);
		else if (opt == 3) printf("%d\n",treap.rank(x));
		else if (opt == 4) printf("%d\n",treap.kth(x));
		else if (opt == 5) printf("%d\n",treap.get_pre(x));
		else printf("%d\n",treap.get_nxt(x));
	}

	return 0;
}

特别鸣谢:@james1BadCreeper 二叉搜索树与平衡树

posted @ 2023-07-06 13:30  Steven24  阅读(73)  评论(0编辑  收藏  举报