首先
如果你不知道
T
r
e
a
p
Treap
Treap
那么
快滚去学Treap
下面这些可以算是预备知识
因为了解
T
r
e
a
p
Treap
Treap的话
F
h
q
Fhq
Fhq理解起来也就会快
当然从零开始也是可以的
可以跳过这一部分
在这简单说一下好了
T
r
e
a
p
Treap
Treap在
B
S
T
BST
BST的基础上,添加了一个修正值。
如果还不太懂
B
S
T
BST
BST的话
来这看看吧———传送门
在满足
B
S
T
BST
BST性质的基础上,
T
r
e
a
p
Treap
Treap节点的修正值还满足最小堆性质
最小堆性质可以被描述为每个子树根节点都小于等于其子节点
然后它的性质:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,而且它的根节点的修正值小于等于左子树根节点的修正值
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值,而且它的根节点的修正值小于等于右子树根节点的修正值
- 它的左、右子树也分别为 T r e a p Treap Treap
那么
说了这些
F
h
q
Fhq
Fhq
T
r
e
a
p
Treap
Treap和普通的
T
r
e
a
p
Treap
Treap的区别和联系在哪呢?
最重要的一点
F
h
q
Fhq
Fhq中没有旋转操作
也叫无旋
T
r
e
a
p
Treap
Treap
而
T
r
e
a
p
Treap
Treap需要大量的旋转操作来维持自身的平衡
只有左旋和右旋
比
s
p
l
a
y
splay
splay要简单一些
F
h
q
Fhq
Fhq的第一个优势就出来了:
代码量少
它的主要操作就是两个
s
p
l
i
t
split
split和
m
e
r
g
e
merge
merge
分别是分裂和合并
不信的自己百度
下面一个一个说
(当然还有其他的常见操作)
咳
忘了还没说初始化
如果你不开结构体的话
像Luogu 3369 普通平衡树这道题
只需要一个记左右儿子的数组
c
h
[
]
[
0
/
1
]
ch[][0/1]
ch[][0/1]
记子树大小的数组
s
i
z
[
]
siz[]
siz[](包括自身)
记权值的数组
v
a
l
[
]
val[]
val[]
还有一个记修正值的数组
c
v
[
]
cv[]
cv[](
C
o
r
r
e
c
t
i
o
n
Correction
Correction
V
a
l
u
e
Value
Value)
下面的讲解是数组版的
指针党相信你们有能力自己写出来
下面开始操作函数的讲解
split
核心思路:
将一棵树拆分成一棵所有节点的值均小于
k
k
k的树和一棵所有节点的值均大于
k
k
k的树
空树将被拆分成两棵空树(边界条件)
在递归过程中,若访问到一个节点的权值大于
k
k
k节点的权值
则说明该节点右子树中所有节点均比
k
k
k大
则将其左子树拆分成一棵所有节点的值均小于
k
k
k的树和一棵所有元素均大于
k
k
k的树
并将其左子树拆分出的大于
k
k
k的树作为该节点的左儿子
则我们得到的两棵树为,
以该节点为根的所有节点的值均大于
k
k
k的树,
和该节点左子树中部分节点构成的所有节点的值均小于
k
k
k的树
反之,
若访问到一个节点的值小于
k
k
k的节点
则说明该节点左子树中所有节点的值均小于
k
k
k
则将其右子树拆分成一棵所有元素的值均小于
k
k
k的树和一棵所有元素的值均大于
k
k
k的树
并将其右子树拆分出的那棵所有元素的值均小于
k
k
k的树作为该节点的右子树
则我们得到的两棵树为,
以该节点为根的所有元素均小于
k
k
k的树,
和该节点右子树中部分节点构成的所有元素均大于
k
k
k的树
void split(int now, int k, int &x, int &y) {
if (!now) { //当前子树是空的,直接返回就好了(边界条件,除了一开始root为0时)
x = y = 0;
return;
}
if (val[now] <= k) { //当前权值与基准数作比较,如果比基准数小
x = now; //当前节点和它的左子树分到左边的树中去
split(ch[now][1], k, ch[now][1], y); //以now的右儿子为根递归右子树
}
else {
y = now; //当前节点和它的右子树分到右边的树中去
split(ch[now][0], k, x, ch[now][0]); //以now的左儿子为根递归左子树
}
update(now); //最后要更新子树大小等信息
}
看参数
简要概括
s
p
l
i
t
split
split函数做的就是把以
n
o
w
now
now为根的子树按
k
k
k为基准数,比
k
k
k小的分到左子树
x
x
x,否则分到右子树
y
y
y
然后
n
o
w
now
now这棵树就被分成了
x
x
x和
y
y
y这两棵树
而且
x
x
x树里的所有点的权值小于
y
y
y树里的所有点的权值(因为这是二叉搜索树)
在一篇博客中还看到一种比较清楚的解释:
比较当前根节点权值
v
a
l
[
n
o
w
]
val[now]
val[now]与
k
k
k值
若大于,则根节点和右子树被划分为
y
y
y树,当前根为
n
o
w
now
now,
y
y
y树的左子树不确定,进入左子树求解子问题
若不大于,则根节点和左子树被划分为
x
x
x树,当前根为
n
o
w
now
now,
x
x
x树的右子树不确定,进入右子树求解子问题
上面代码因为要加注释
太长了,缩一下,也就是我日常的写法
void split(int now, int k, int &x, int &y) {
if (!now) {x = y = 0; return;}
if (val[now] <= k) x = now, split(ch[now][1], k, ch[now][1], y);
else y = now, split(ch[now][0], k, x, ch[now][0]);
update(now);
} //这样还是挺漂亮的
注意上面这是按权值分裂
按排名(size)分裂简单提一下
思想是一样的
把前面的
k
k
k个放到左子树里
后面的都放在右子树里
void split(int now, int k, int &x, int &y) {
if (!now) {x = y = 0; return;}
if (k <= siz[ch[now][0]]) y = now, split(ch[now][0], k, x, ch[now][0]);
else x = now, split(ch[now][1], k - siz[ch[now][0]] - 1, ch[now][1], y);
update(now);
}
merge
F
h
q
Fhq
Fhq的另一个重要操作,合并
在
T
r
e
a
p
Treap
Treap中,父节点的修正值小于两个子节点的修正值
int merge(int x, int y) { //注意这个操作的返回值是根节点
if (!x or !y) return x + y; //如果有一棵树是空的,返回另一棵树就可以
if (cv[x] < cv[y]) { //比较修正值
tree[x][1] = merge(tree[x][1], y); //把y合并到x的右子树
update(x);
return x;
}
else {
tree[y][0] = merge(x, tree[y][0]); //把x合并到y的左子树
update(y);
return y;
}
}
m
e
r
g
e
merge
merge函数是把以
x
x
x为根的子树和以
y
y
y为根的子树合并,返回新的根节点
注意以
x
x
x值为根的子树的最大节点小于以
y
y
y为根的子树的最小节点(因为二叉查找树左右孩子的定义)
也就是说这两棵子树本来就是一棵完全小于另外一棵
所以我们只需要在合并时保证堆性质即可
这个相对来说好理解
然后就是对题目而言的其他操作
下面先说完其他的附属函数
update
记得每次重构完子树都 u p d a t e update update一下
void update(int x) {
siz[x] = 1 + siz[tree[x][0]] + siz[tree[x][1]];
}
randoom
这里的随机数你直接用
r
a
n
d
(
)
rand()
rand()也可以
但是在效率上会有差别
空间和时间上手写的都会更优
特别是空间上
int randoom() {
return rand() << 15 | rand();
}
newnode
就是新建节点
int newnode(int x) {
siz[++cnt] = 1; //子树大小默认为1
val[cnt] = x; //记下权值
cv[cnt] = randoom(); //随机一个修正值
return cnt; //返回根
}
在这里介绍Luogu 3369 普通平衡树中的几种操作
插入 a a a
这些就不用写函数了
直接在输入后面用就行
r
o
o
t
root
root刚开始默认为
0
0
0
split(root, a, x, y); //先把树分裂开
root = merge(merge(x ,newnode(a)), y); //合并后返回新根的编号
删除 a a a
split(root, a, x, z); //分裂开原树
split(x, a - 1, x, y); //在按a-1分裂一次,这就只孤立出了要删的那个节点
y = merge(tree[y][0], tree[y][1]); //把其余的合并起来
root = merge(merge(x, y), z); //返回新根
查询 a a a的排名
split(root, a - 1, x, y); //很显然
printf("%d\n", siz[x] + 1); //下面的子树大小就是a的排名
root = merge(x, y);
查询排名为 a a a的数
printf("%d\n", val[kth(root, a)]); //找到这个点输出出来就好
求 a a a的前驱
split(root, a - 1, x, y); //自己感性理解
printf("%d\n", val[kth(x, siz[x])]);
root = merge(x, y);
求 a a a的后继
split(root, a, x, y); //同上qwq
printf("%d\n", val[kth(y, 1)]);
root = merge(x, y);
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <complex>
#include <algorithm>
#include <climits>
#include <queue>
#include <map>
#include <set>
#include <vector>
#include <iomanip>
#define A 1000010
#define B 2010
using namespace std;
typedef long long ll;
int tree[A][2], val[A], cv[A], siz[A], cnt;
int n, a, opt, root, x, y, z;
void update(int x) {
siz[x] = 1 + siz[tree[x][0]] + siz[tree[x][1]];
}
int randoom() {
return rand() << 15 | rand();
}
int newnode(int x) {
siz[++cnt] = 1;
val[cnt] = x;
cv[cnt] = randoom();
return cnt;
}
void split(int now, int k, int &x, int &y) {
if (!now) {x = y = 0; return;}
if (val[now] <= k) x = now, split(tree[now][1], k, tree[now][1], y);
else y = now, split(tree[now][0], k, x, tree[now][0]);
update(now);
}
int merge(int x, int y) {
if (!x or !y) return x + y;
if (cv[x] < cv[y]) {
tree[x][1] = merge(tree[x][1], y);
update(x);
return x;
}
else {
tree[y][0] = merge(x, tree[y][0]);
update(y);
return y;
}
}
int kth(int now, int k) {
if (k <= siz[tree[now][0]]) kth(tree[now][0], k);
else if (k == siz[tree[now][0]] + 1) return now;
else kth(tree[now][1], k - siz[tree[now][0]] - 1);
}
int main() {
srand(time(0));
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &opt, &a);
switch(opt) {
case 1 : split(root, a, x, y); root = merge(merge(x ,newnode(a)), y); break;
case 2 : split(root, a, x, z); split(x, a - 1, x, y); y = merge(tree[y][0], tree[y][1]); root = merge(merge(x, y), z); break;
case 3 : split(root, a - 1, x, y); printf("%d\n", siz[x] + 1); root = merge(x, y); break;
case 4 : printf("%d\n", val[kth(root, a)]); break;
case 5 : split(root, a - 1, x, y); printf("%d\n", val[kth(x, siz[x])]); root = merge(x, y); break;
case 6 : split(root, a, x, y); printf("%d\n", val[kth(y, 1)]); root = merge(x, y); break;
default : break;
}
}
return 0;
}