【Coel.做题笔记】【开学-重启】无旋转二叉搜索堆(FHQ-Treap)

题前碎语

回来啦!
虽然其实在寒假做了很多很多题<-做了题你也不写博客
新学期到了一个完全不认识的班,只能继续努力啦!
距离\(CSP\)还有不到200天,加油吧!

题目简介

P3369 【模板】普通平衡树
洛谷传送门

题目描述

您需要写一种数据结构(也就是普通平衡树),来维护一些数,其中需要提供以下操作:

  1. 插入 \(x\)
  2. 删除 \(x\) 数(若有多个相同的数,因只删除一个)
  3. 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
  4. 查询排名为 \(x\) 的数
  5. \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
  6. \(x\) 的后继(后继定义为大于 \(x\),且最小的数)

输入格式

第一行为 \(n\),表示操作的个数,下面 \(n\) 行每行有两个数 \(\text{opt}\)\(x\)\(\text{opt}\) 表示操作的序号( \(1 \leq \text{opt} \leq 6\) )。

输出格式

对于操作 \(3,4,5,6\) 每行输出一个数,表示对应答案。


正文

平衡树有很多写法,例如旋转二叉搜索堆(\(Treap\)),伸展树(\(Splay\)),红黑树(\(Red\) \(Black\) \(Tree\))等等。在某位学长大佬的介绍下,我选择了无旋转二叉搜索堆(\(FHQ-Treap\),也就是范浩强\(Treap\))。

基本思路

平衡树是二叉搜索树的变种。
一般的二叉搜索树也支持查前驱后继、查排名等基本操作。一般情况下,二叉搜索树的查询效率能保持在\(O(logn)\),但如果故意构造插入节点的顺序,可能使得二叉搜索树退化成一条链,效率变成\(O(n)\)
因此,平衡树通过各种各样的方式保证二叉搜索树不被退化,从而把效率优化回到\(O(logn)\)
\(Treap\)如何保持效率呢?给每个节点额外加上一个随机权值,并且把二叉搜索树维护到具有堆的性质
一般的\(Treap\)采用的是旋转维护堆,而\(FHQ-Treap\)采取的方法是分裂-合并维护堆

操作合集

初始化

为了方便调用,我们把整个数据结构封装到一个结构体里。

int n, root;
struct FHQ_Treap {
	int cnt;
	int ch[maxn][2], val[maxn], pri[maxn], size[maxn];
	//ch[i][0]-左子树,ch[i][1]-右子树
}

其中,\(val\)是节点原本的数值,\(pri\)为随机权值,\(size\)为该节点对应的子树节点数。
开设一个全局变量\(root\)表示树根的编号,\(cnt\)表示总节点数。

pushup维护size

左子树\(+\)右子树\(+1\)(节点自己)

inline void pushup(int x) {
	size[x] = size[ch[x][0]] + size[ch[x][1]] + 1;
}

New_node新建节点

\(cnt+1\),维护对应的左右子树、权值和数值。

void New_node(int &id, int v) {
	size[++cnt] = 1;
	val[cnt] = v;
	pri[cnt] = rand();
	ch[cnt][0] = ch[cnt][1] = 0;
	id = cnt;
}

下面是核心操作,仔细看看。

split分裂操作

先扔代码。

void split(int id, int k, int &x, int &y) {
	if (id == 0)
		x = y = 0;
	else {
		if (val[id] <= k) {
			x = id;
			split(ch[id][1], k, ch[id][1], y);
			pushup(x);
	}
		else {
			y = id;
			split(ch[id][0], k, x, ch[id][0]);
			pushup(y);
		}
	}
}

按照节点的权值\(k\)来分裂树,\(id\)为节点编号,\(x\)为左子树的根,\(y\)为右子树的根。
如果当前节点编号比权值小,则把左子树的根确定为当前编号,并且继续分裂右子树,维护权值;
如果当前节点编号比权值大,则确定右子树,分裂左子树。
如果编号为0,则已到达末尾,左右子树都为0。

merge合并操作

为什么不是\(assign\)

int merge(int x, int y) {
	if (x == 0 || y == 0)
		return x + y;
	if (pri[x] < pri[y]) {
		ch[x][1] = merge(ch[x][1], y);
		pushup(x);
		return x;
		}
	else {
		ch[y][0] = merge(x, ch[y][0]);
		pushup(y);
		return y;
	}
}

和分裂类似,但多了一个返回值,为合并后根节点的编号。
如果右子树的权值更大,合并右子树;
如果左子树的权值更大,合并左子树。
如果左右子树里有一个为空,则已到达末尾,根节点编号即为左右子树之和(也可以写成x | y,因为两个子树里有一个是空的)
核心操作结束

insert插入

按照节点值分裂子树,新建节点,再合并。

inline void insert(int res) {
	int x, y, z;
	x = y = z = 0;
	split(root, res, x, y);
	New_node(z, res);
	root = merge(merge(x, z), y);
}

erase删除

把要删除的节点独立出来,删除中间段,再将左右两端合并。

inline void erase(int res) {
	int x, y, z;
	x = y = z = 0;
	split(root, res, x, z);
	split(x, res - 1, x, y);
	y = merge(ch[y][0], ch[y][1]);
	root = merge(merge(x, y), z);
}

查询

放到一起说。
按照排行查值:如果左子树+1就是排行,意味着已经找到,直接返回。否则在两边子树继续找。

int Query_Num(int id, int rank) {
	if (rank == size[ch[id][0]] + 1)
		return val[id];
	else if (rank <= size[ch[id][0]])
		return Query_Num(ch[id][0], rank);
	else
		return Query_Num(ch[id][1], rank - size[ch[id][0]] - 1);
}

按照值查排行:按数值分裂,左子树+1就是结果。

int Query_Rank(int res) {
	int x, y, ans;
	split(root, res - 1, x, y);
	ans = size[x] + 1;
	root = merge(x, y);
	return ans;
}

查前驱:按数值分裂,利用查数值找前驱,找完合并。

int Query_Pre(int res) {
	int x, y, k, ans;
	split(root, res - 1, x, y);
	if (x == 0)
		return -inf;
	k = size[x];
	ans = Query_Num(x, k);
	root = merge(x, y);
	return ans;
}

查后继同理。

int Query_Pos(int res) {
	int x, y, ans;
	split(root, res, x, y);
	if (y == 0)
		return inf;
	else
		ans = Query_Num(y, 1);
	root = merge(x, y);
	return ans;
	}

代码

#include <cstdio>
#include <iostream>
#include <cctype>
#include <cstdlib>

using namespace std;

const int maxn = 1e5 + 10, inf = 1e9;

int n, root;

struct FHQ_Treap {
	int cnt;
	int ch[maxn][2], val[maxn], pri[maxn], size[maxn];
	inline void pushup(int x) {
		size[x] = size[ch[x][0]] + size[ch[x][1]] + 1;
	}
	void New_node(int &id, int v) {
		size[++cnt] = 1;
		val[cnt] = v;
		pri[cnt] = rand();
		ch[cnt][0] = ch[cnt][1] = 0;
		id = cnt;
	}
	int merge(int x, int y) {
		if (x == 0 || y == 0)
			return x + y;
		if (pri[x] < pri[y]) {
			ch[x][1] = merge(ch[x][1], y);
			pushup(x);
			return x;
		}
		else {
			ch[y][0] = merge(x, ch[y][0]);
			pushup(y);
			return y;
		}
	}
	void split(int id, int k, int &x, int &y) {
		if (id == 0)
			x = y = 0;
		else {
			if (val[id] <= k) {
				x = id;
				split(ch[id][1], k, ch[id][1], y);
				pushup(x);
			}
			else {
				y = id;
				split(ch[id][0], k, x, ch[id][0]);
				pushup(y);
			}
		}
	}
	inline void insert(int res) {
		int x, y, z;
		x = y = z = 0;
		split(root, res, x, y);
		New_node(z, res);
		root = merge(merge(x, z), y);
	}
	inline void erase(int res) {
		int x, y, z;
		x = y = z = 0;
		split(root, res, x, z);
		split(x, res - 1, x, y);
		y = merge(ch[y][0], ch[y][1]);
		root = merge(merge(x, y), z);
	}
	int Query_Rank(int res) {
		int x, y, ans;
		split(root, res - 1, x, y);
		ans = size[x] + 1;
		root = merge(x, y);
		return ans;
	}
	int Query_Num(int id, int rank) {
		if (rank == size[ch[id][0]] + 1)
			return val[id];
		else if (rank <= size[ch[id][0]])
			return Query_Num(ch[id][0], rank);
		else
			return Query_Num(ch[id][1], rank - size[ch[id][0]] - 1);
	}
	int Query_Pre(int res) {
		int x, y, k, ans;
		split(root, res - 1, x, y);
		if (x == 0)
			return -inf;
		k = size[x];
		ans = Query_Num(x, k);
		root = merge(x, y);
		return ans;
	}
	int Query_Pos(int res) {
		int x, y, ans;
		split(root, res, x, y);
		if (y == 0)
			return inf;
		else
			ans = Query_Num(y, 1);
		root = merge(x, y);
		return ans;
	}
}FHQ_Treap;

inline int read() {
	int x = 0, f = 1;
	char ch = getchar();
	while (!isdigit(ch)) {
		if (ch == '-')  f = -1;
		ch = getchar();
	}
	while (isdigit(ch)) {
		x = x * 10 + ch - '0';
		ch = getchar();
	}
	return x * f;
}

int main() {
	srand(2109);
	n = read();
	while (n--) {
		int op = read(), res = read();
		if (op == 1)
			FHQ_Treap.insert(res);
		else if (op == 2)
			FHQ_Treap.erase(res);
		else if (op == 3)
			printf("%d\n", FHQ_Treap.Query_Rank(res));
		else if (op == 4)
			printf("%d\n", FHQ_Treap.Query_Num(root, res));
		else if (op == 5)
			printf("%d\n", FHQ_Treap.Query_Pre(res));
		else
			printf("%d\n", FHQ_Treap.Query_Pos(res));
	}
	return 0;
}

题后闲话

花了差不多一个小时写了个笔记,希望自己过几天能看得懂
别的也没什么好说的,新学期继续努力吧!

posted @ 2022-03-03 19:27  秋泉こあい  阅读(29)  评论(0编辑  收藏  举报