前置技能
二叉搜索树
堆
前置技能有旋转treap?不存在的(我到现在仍然不会旋转的treap qwq)
Treap简介
引用维基百科上一句精辟的话:Treap=Tree+Heap
在Treap上需要维护两个值:一个优先级
p
r
i
pri
pri,一个节点权值
v
a
l
val
val。
其中优先级取随机数,满足小根堆的性质。节点权值满足二叉搜索树的性质。
即每个节点的
p
r
i
pri
pri值均小于左、右孩子的
p
r
i
pri
pri值,每个节点的左子树的
v
a
l
val
val均小于这个节点的
v
a
l
val
val值,右子树的
v
a
l
val
val大于这个节点的
v
a
l
val
val值。
示例:
显然,Treap的每棵子树都是一棵Treap。
珂以证明(虽然我不会证qwq),Treap的树高为
l
o
g
n
logn
logn。
普通的Treap是通过旋转来维护其性质的,而FHQ Treap通过合并(Merge)和分裂(Split)来维护qwq。
Merge操作
比如要合并两棵分别以
x
,
y
x,y
x,y为根的树,假设
x
x
x的
v
a
l
val
val值<=
y
y
y的
v
a
l
val
val值。
然后按优先级决定谁是根qwq,当
x
x
x的
p
r
i
pri
pri值比
y
y
y的
p
r
i
pri
pri值小时,
x
x
x为新的根,否则
y
y
y为新的根,这样珂以使
p
r
i
pri
pri仍然满足小根堆的性质。
当
x
x
x的
p
r
i
pri
pri值更小时,
x
x
x的
v
a
l
val
val值小于
y
y
y的
v
a
l
val
val值,根据二叉搜索树的性质,应该把
y
y
y放到右子树中。因此把
x
x
x的右孩子与
y
y
y合并。
当
y
y
y的
p
r
i
pri
pri值更小时,
x
x
x的
v
a
l
val
val值小于
y
y
y的
v
a
l
val
val值,所以应该把
x
x
x放到左子树中,因此把
y
y
y的左孩子与
x
x
x合并。
最后需要更新根节点的
s
i
z
siz
siz值qwq(即子树内的节点个数)
Merge 的毒瘤代码:
inline void Update(int x) { //更新子树大小
tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
int Merge(int x,int y) {
//合并以x和y为根的树
if(!x || !y) return x+y;
//用优先级决定新的根是x还是y
if(tree[x].pri<tree[y].pri) {
rc(x)=Merge(rc(x),y);
Update(x);
return x;
} else {
lc(y)=Merge(x,lc(y));
Update(y);
return y;
}
}
Split操作
这里讲述的是把一棵树按权值分为两棵子树的方法。
假设要把
i
i
i的子树中权值
<
=
k
<=k
<=k的分到以
x
x
x为根的Treap中,剩下的权值
>
k
>k
>k的分到以
y
y
y为根的Treap中。
当
i
i
i的权值
<
=
k
<=k
<=k时,根据二叉搜索树的性质,
i
i
i的左子树中的所有节点权值均
<
=
k
<=k
<=k。
所以把
i
i
i和
i
i
i的左子树内的所有节点都分给
x
x
x,然后递归把
i
i
i的右子树中
<
=
k
<=k
<=k的分到
x
x
x的右子树(这样仍然满足
v
a
l
val
val为二叉搜索树的性质),
i
i
i的右子树中
>
k
>k
>k的分给
y
y
y即可。
当
i
i
i的权值
>
k
>k
>k时,
i
i
i的右子树中的所有节点权值均
>
k
>k
>k。
同理,把
i
i
i和
i
i
i右子树内的所有节点均分给
y
y
y,然后递归把
i
i
i的左子树中的
>
k
>k
>k的分到
y
y
y的左子树,
i
i
i的左子树中
<
=
k
<=k
<=k的分给
x
x
x即可qwq。
Split 的毒瘤代码:
void Split(int i,int k,int &x,int &y) {
//把以i为根的树分成以x为根和以y为根的树
//把所有权值<=k的分到x中,>k的分到y中
if(!i) {
//若i为空节点,则x和y也为空
x=y=0;
} else {
if(tree[i].val<=k) {
//这里先让x=i,然后递归下去就把i的右子树中<=k的分到了rc(x)
x=i;
Split(rc(i),k,rc(x),y);
} else {
//同理
y=i;
Split(lc(i),k,x,lc(y));
}
Update(i); //记得更新子树大小
}
}
找权值第k大
因为Treap的
v
a
l
val
val满足二叉搜索树性质,所以类似地按照二叉搜索树的方法找即可。
比较巧妙的一点是:
t
r
e
e
[
l
c
(
i
)
]
.
s
i
z
+
1
tree[lc(i)].siz+1
tree[lc(i)].siz+1表示左子树加上根这个节点的总节点数qwq。
代码:
int kth(int i,int k) {
//找到i的子树中val第k大的点的下标
while(true) {
if(k<=tree[lc(i)].siz) {
//若k比左子树大小还小
//则第k大显然在左子树中
i=lc(i);
} else if(k>tree[lc(i)].siz+1) {
//同理
k-=tree[lc(i)].siz+1;
i=rc(i);
} else {
return i;
}
}
}
另外放一个新建节点的New函数代码:
inline int New(int v) { //新建节点并返回其下标
tree[++tot].val=v;
tree[tot].pri=rand();
tree[tot].siz=1;
return tot;
}
例题
洛谷P3369 【模板】普通平衡树
即 LOJ #104 普通平衡树
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1.插入
x
x
x数
2.删除
x
x
x数(若有多个相同的数,因只删除一个)
3.查询
x
x
x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
4.查询排名为
x
x
x的数
5.求
x
x
x的前驱(前驱定义为小于
x
x
x,且最大的数)
6.求
x
x
x的后继(后继定义为大于
x
x
x,且最小的数)
这里讲一下操作1和操作2,其他操作比较简单,看代码注释就珂以了qwq(我不会告诉你只是我懒得写qwq)
操作1:
因为Merge操作是需要
x
x
x的权值
<
=
y
<=y
<=y的权值,所以不能直接New一个节点然后直接大莉Merge qwq。
假设当前需要插入的节点权值为num,我们考虑把这棵Treap按照权值分为两棵树,权值
<
=
n
u
m
<=num
<=num的分到
x
x
x,
>
n
u
m
>num
>num的分到
y
y
y。
所以珂以把
x
x
x和New得到的节点合并,再把得到的结果与
y
y
y合并。
操作2:
假设要删掉一个权值为
n
u
m
num
num的节点。
这里珂以考虑把所有权值为
n
u
m
num
num的节点分到一棵树中,然后删根节点。
具体实现方法:把所有权值
<
=
n
u
m
<=num
<=num的节点分到
x
x
x,权值
>
n
u
m
>num
>num的分到
z
z
z。
然后把
z
中
\color{red}z中
z中权值
<
=
n
u
m
−
1
<=num-1
<=num−1的分到
x
x
x,权值
>
=
n
u
m
>=num
>=num的分到
y
y
y。
不难发现这样
y
y
y中存的都是权值为
n
u
m
num
num的点,此时把根节点删掉即可qwq
例题代码
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<vector>
#define re register int
#define lc(x) tree[x].ls
#define rc(x) tree[x].rs
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
const int Size=200005;
struct node {
int ls,rs; //左右孩子
int val; //权值,左<根<右
int pri; //优先级,维护小根堆
int siz; //子树大小
} tree[Size];
int n,tot;
inline void Update(int x) { //更新子树大小
tree[x].siz=tree[lc(x)].siz+tree[rc(x)].siz+1;
}
inline int New(int v) { //新建节点并返回其下标
tree[++tot].val=v;
tree[tot].pri=rand();
tree[tot].siz=1;
return tot;
}
int Merge(int x,int y) {
//合并以x和y为根的树
if(!x || !y) return x+y;
//用优先级决定新的根是x还是y
if(tree[x].pri<tree[y].pri) {
rc(x)=Merge(rc(x),y);
Update(x);
return x;
} else {
lc(y)=Merge(x,lc(y));
Update(y);
return y;
}
}
void Split(int i,int k,int &x,int &y) {
//把以i为根的树分成以x为根和以y为根的树
//把所有权值<=k的分到x中,>k的分到y中
if(!i) {
//若i为空节点,则x和y也为空
x=y=0;
} else {
if(tree[i].val<=k) {
//这里先让x=i,然后递归下去就把i的右子树中<=k的分到了rc(x)
x=i;
Split(rc(i),k,rc(x),y);
} else {
//同理
y=i;
Split(lc(i),k,x,lc(y));
}
Update(i); //记得更新子树大小
}
}
int kth(int i,int k) {
//找到i的子树中val第k大的点的下标
while(true) {
if(k<=tree[lc(i)].siz) {
//若k比左子树大小还小
//则第k大显然在左子树中
i=lc(i);
} else if(k>tree[lc(i)].siz+1) {
//同理
k-=tree[lc(i)].siz+1;
i=rc(i);
} else {
return i;
}
}
}
inline int Get_K(int rt,int rk) {
//得到第k大的点的权值
return tree[kth(rt,rk)].val;
}
int main() {
//暴力**不可避
srand(19260817);
n=read();
int root=0;
int x=0,y=0,z=0;
while(n--) {
int op=read();
int num=read();
if(op==1) {
//插入num
Split(root,num,x,y);
root=Merge(Merge(x,New(num)),y);
} else if(op==2) {
//删除num
Split(root,num,x,z);
//此时x中存了所有权值<=num的点
Split(x,num-1,x,y);
//此时x中存了所有权值<=num-1的点
//因此y中存了所有权值为num的点
//然后把y的根节点删掉
y=Merge(lc(y),rc(y));
root=Merge(Merge(x,y),z);
} else if(op==3) {
//查询num的排名
//x中存的是所有权值<=num-1的点
Split(root,num-1,x,y);
//因此x的大小+1就是num的排名
printf("%d\n",tree[x].siz+1);
root=Merge(x,y);
} else if(op==4) {
//查询第num小的数
printf("%d\n",Get_K(root,num));
} else if(op==5) {
//查询num的前驱
//x存的是所有权值<=num-1的点
//x中第(x子树大小)小的点就是num前驱
Split(root,num-1,x,y);
printf("%d\n",Get_K(x,tree[x].siz));
root=Merge(x,y);
} else {
//x的后继(基本同理)
//y存的是所有权值>num的点
//因此y中第一小的就是num前驱
Split(root,num,x,y);
printf("%d\n",Get_K(y,1));
root=Merge(x,y);
}
}
return 0;
}