【Coel.做题笔记】【开学-重启】无旋转二叉搜索堆(FHQ-Treap)
题前碎语
回来啦!
虽然其实在寒假做了很多很多题<-做了题你也不写博客
新学期到了一个完全不认识的班,只能继续努力啦!
距离\(CSP\)还有不到200天,加油吧!
题目简介
P3369 【模板】普通平衡树
洛谷传送门
题目描述
您需要写一种数据结构(也就是普通平衡树),来维护一些数,其中需要提供以下操作:
- 插入 \(x\) 数
- 删除 \(x\) 数(若有多个相同的数,因只删除一个)
- 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
- 查询排名为 \(x\) 的数
- 求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
- 求 \(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;
}
题后闲话
花了差不多一个小时写了个笔记,希望自己过几天能看得懂
别的也没什么好说的,新学期继续努力吧!