问题引入:Luogu P3369 普通平衡树
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入 x x x 数
- 删除 x x x 数(若有多个相同的数,因只删除一个)
- 查询 x x x 数的排名(排名定义为比当前数小的数的个数 + 1 +1 +1 )
- 查询排名为 x x x 的数
- 求 x x x 的前驱(前驱定义为小于 x x x,且最大的数)
- 求 x x x 的后继(后继定义为大于 x x x,且最小的数)
拿到这道题,你觉得暴力该怎么做?
v
e
c
t
o
r
vector
vector 二分查找 + 暴力插入可以拿到
60
60
60 分,为什么会只有
60
60
60 呢?思考一下,我们的瓶颈在于插入是
O
(
n
)
O(n)
O(n) 的,那么什么数据结构可以做到复杂度很低的插入呢? 那就是树。
考虑建立一颗二叉搜索树,对于任意节点 p p p 满足 v a l l c p < v a l p < v a l r c p val_{lc_p}<val_p<val_{rc_p} vallcp<valp<valrcp ,其中 l c p lc_p lcp 与 r c p rc_p rcp 分别指 p p p 的左儿子和右儿子。
那么给出一组测试数据,看看建成的树会是什么样子:
给出操作
4
4
4 的代码:
inline int findxth(int root,int x)
{
if(siz[L[root]]+1==x) return root;
if(siz[L[root]]>=x) return findkth(L[root],x);
return findkth(R[root],x-siz[L[root]]-1);
}
在代码中, s i z siz siz 树组保存的是子树的大小, L L L 与 R R R 分别储存做儿子与右儿子的下标,所以当前数在子树中的排名是 s i z L r o o t + 1 siz_{L_{root}}+1 sizLroot+1,那么分类讨论:
- x ≤ s i z L r o o t x\leq siz_{L_{root}} x≤sizLroot,那么以 r o o t root root 为根的的子树中,第 x x x 大是其左子树上的第 x x x 大;
- x = s i z L r o o t + 1 x=siz_{L_{root}}+1 x=sizLroot+1,那么以 r o o t root root 为根的的子树中,第 x x x 大就是 r o o t root root
- x > s i z L r o o t + 1 x> siz_{L_{root}}+1 x>sizLroot+1,那么那么以 r o o t root root 为根的的子树中,第 x x x 大是其左子树上的第 x − s i z L r o o t − 1 x-siz_{L_{root}}-1 x−sizLroot−1 大。
思考一下为什么最后是 x − s i z L r o o t − 1 x-siz_{L_{root}}-1 x−sizLroot−1:其实是因为前 s i z L r o o t + 1 siz_{L_{root}}+1 sizLroot+1 个都在左子树与根上。
那么怎么卡掉这种数据结构呢?
每次插入数单调递增或递减,那么就会退化成这样的链:
那么复杂度就退化为
O
(
n
)
O(n)
O(n) 的了,还不如写个暴力。
那我们怎么解决这种情况呢? 我们需要代入堆的性质:
对于一个堆,根节点的权一定小于任意儿子的权。
怎么把这个性质代入二叉搜索树中呢?对于普通的 T r e a p Treap Treap ,我们引入两种旋转操作,左旋与右旋,每个节点赋予一个随机的权,通过不断的左旋与右旋,满足其堆的性质,并且保留其二叉搜索树的性质,那么这棵树是期待平衡的。
但在这片文章中,我们不讨论左旋与右旋,运用大佬 F h q Fhq Fhq 自创的 F h q − T r e a p Fhq-Treap Fhq−Treap ,来维护其堆的性质。
先来看其核心操作:分裂与合并:
Split 分裂
分裂有两种,分别为 按值分裂 与 按排名分裂
按值分裂
按值分裂指把一颗树按给定的权值 p o s pos pos 分裂成两颗子树,我们称为 左树 与右树,满足:左树上的权值都小于等于 p o s pos pos ,右树上的权值大于 p o s pos pos,那么怎么处理呢?
比如在当前树中:
我们按
p
o
s
=
15
pos=15
pos=15 分割:
首先,这个东西肯定是递归的,我们先尝试对根处理:
24
>
15
24>15
24>15 ,那么它一定属于右树的:
我们突然现,因为 24 > 15 24>15 24>15 ,由于二叉搜索树的性质,右儿子的权值总是比根大,那么 24 24 24 的右子树上的所有权值一定是大于 15 15 15 的,那么它们都属于右子树的:
我们现在只需要对
24
24
24 的左子树进行操作:
发现
13
<
15
13<15
13<15,那么
13
13
13 的左子树上的权值也一定小于
15
15
15,那么分裂树变成这样:
对
13
13
13 的右子树,即
14
14
14 进行判断,那么分裂结果如下:
给出递归代码:
inline void split(int p,int v,int &l,int &r){
if(!p) {l=r=0;return;}
//没有了,返回
else if(val[p]<=v) l=p,split(R[p],v,R[p],r);
//当前节点属于左树,那么左子树也一定属于左树,只需在右子树中再递归判断
else r=p,split(L[p],v,l,L[p]);
//同理
upd(p);
}
此处
l
,
r
l,r
l,r 是引用的,那么只需在外定义两个整数,在分裂时当做参数传入,执行完
l
,
r
l,r
l,r 即为左右树的根。
F
h
q
−
t
r
e
a
p
Fhq-treap
Fhq−treap 的好处在于你只需要想好一半,另一半只需要复制粘贴再改为相反的即可。
注: u p d upd upd 函数是用来维护子树大小的,给出代码:
inline void upd(int p)
{
siz[p]=siz[L[p]]+siz[R[p]]+1;
}
按排名分裂
给出代码,原理同上给出操作 4 4 4 的代码,自行思考:
inline void split(int p, int k, int &x, int &y)
{
if(!p){x = y = 0;return;}
else if(r <= siz[L[p]]) y=p,split(L[p],k,x,L[p]);
else x=id,split(R[p],k-siz[L[p]]-1,R[p],y);
upd(p);
}
那么 F h q − T r e a p Fhq-Treap Fhq−Treap 核心代码已经写完,接下来切题
Luogu P3369 普通平衡树:
- 插入一个数 x x x
inline void ins(int a)
{
split(rt,a,x,y);
cre(tmp,a);
rt=merge(merge(x,tmp),y);
}
按值
a
a
a 分裂为两个树,创立新节点,节点序号为
t
m
p
tmp
tmp(此处为引用),值为
a
a
a。
给出
c
r
e
cre
cre 创建节点代码:
inline void cre(int &p,int v){
val[++tot]=v;
siz[tot]=1;
rand(rd[tot]);
p=tot;
}
t o t tot tot 为当前的节点数, r a n d rand rand 中的参数为引用的,即为给其赋予一个伪随机值,给出 r a n d rand rand 函数代码:
int rnd=1345252;
void rand(int &x){x=(rnd=(rnd*123044)%1000000000);}
- 删除一个数 x x x
inline void del(int a){
split(rt,a,x,y);
split(x,a-1,x,z);
z=merge(L[z],R[z]);
rt=merge(merge(x,z),y);
}
经过两次分裂, z z z 树上所有节点权值都为 x x x,因为只需要删除一个,那么考虑删除根,合并左右节点,记得再合并回去,此处 r t rt rt 为整树的根
- 查询 x x x 数的排名
inline int findrank(int a){
split(rt,a-1,x,y);
tmp=siz[x]+1;
rt=merge(x,y);
return tmp;
}
分裂后左树上所有节点权小于 x x x ,右树上所有节点权值大于等于 x x x ,那么 x x x 的排名即为左树大小 + 1 +1 +1
- 与二叉搜索树原理相同
inline int findkth(int p,int k){
if(siz[L[p]]+1==k) return p;
if(siz[L[p]]>=k) return findkth(L[p],k);
return findkth(R[p],k-siz[L[p]]-1);
}
- 找 x x x 数的前驱
inline int front(int a){
split(rt,a-1,x,y);
tmp=findkth(x,siz[x]);
rt=merge(x,y);
return tmp;
}
即为分裂后左树(左树权值小于 x x x) 中,排名为左树大小的权值
- 找 x x x 数的后继
inline int back(int a){
split(rt,a,x,y);
tmp=findkth(y,1);
rt=merge(x,y);
return tmp;
}
思路与 5 5 5 无异
注意:分裂后一定要合并回来
给出完整代码:
#include <stdio.h>
#define Maxn 200004
int L[Maxn],R[Maxn];
int val[Maxn],siz[Maxn],rd[Maxn],tmp;
int rt,tot,x,y,z,rnd=1345252;
void rand(int &x){x=(rnd=(rnd*123044)%1000000000);}
inline void upd(int p){
siz[p]=siz[L[p]]+siz[R[p]]+1;
}
inline void cre(int &p,int v){
val[++tot]=v;
siz[tot]=1;
rand(rd[tot]);
p=tot;
}
inline void split(int p,int v,int &l,int &r){
if(!p) {l=r=0;return;}
else if(val[p]<=v) l=p,split(R[p],v,R[p],r);
else r=p,split(L[p],v,l,L[p]);
upd(p);
}
inline int merge(int l,int r){
if(!l||!r) return l+r;
if(rd[l]<rd[r]){
R[l]=merge(R[l],r);upd(l);
return l;
}
L[r]=merge(l,L[r]); upd(r);
return r;
}
inline void ins(int a){
split(rt,a,x,y);
cre(tmp,a);
rt=merge(merge(x,tmp),y);
}
inline void del(int a){
split(rt,a,x,y);
split(x,a-1,x,z);
z=merge(L[z],R[z]);
rt=merge(merge(x,z),y);
}
inline int findrank(int a){
split(rt,a-1,x,y);
tmp=siz[x]+1;
rt=merge(x,y);
return tmp;
}
inline int findkth(int p,int k){
if(siz[L[p]]+1==k) return p;
if(siz[L[p]]>=k) return findkth(L[p],k);
return findkth(R[p],k-siz[L[p]]-1);
}
inline int front(int a){
split(rt,a-1,x,y);
tmp=findkth(x,siz[x]);
rt=merge(x,y);
return tmp;
}
inline int back(int a){
split(rt,a,x,y);
tmp=findkth(y,1);
rt=merge(x,y);
return tmp;
}
int main(){
int Q,opt,a;
scanf("%d",&Q);
while(Q--)
{
scanf("%d%d",&opt,&a);
if(opt==1) ins(a);
else if(opt==2) del(a);
else if(opt==3) printf("%d\n",findrank(a));
else if(opt==4) printf("%d\n",val[findkth(rt,a)]);
else if(opt==5) printf("%d\n",val[front(a)]);
else printf("%d\n",val[back(a)]);
}
}