模板题目传送门
首先,我们介绍一下 Treap
###Treap(树堆)
Treap=Tree+Heap。Treap本身是一棵二叉搜索树,它的左子树和右子树也分别是一个Treap,和一般的二叉搜索树不同的是,Treap纪录一个额外的数据,就是优先级。Treap在以关键码构成二叉搜索树的同时,还满足堆的性质。这些优先级是是在结点插入时,随机赋予的,Treap根据这些优先级满足堆的性质。这样的话,Treap是有一个随机附加域满足堆的性质的二叉搜索树,其结构相当于以随机数据插入的二叉搜索树。其基本操作的期望时间复杂度为O(
l
o
g
n
log_n
logn)。相对于其他的平衡二叉搜索树,Treap的特点是实现简单,且能基本实现随机平衡的结构。
Treap维护堆性质的方法只用到了旋转,只需要两种旋转,编程复杂度比Splay要小一些。
然后我们看一道模板题。。。
###题目大意
题目描述
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1:插入x数
2:删除x数(若有多个相同的数,因只删除一个)
3:查询x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
4:查询排名为x的数
5:求x的前驱(前驱定义为小于x,且最大的数)
6:求x的后继(后继定义为大于x,且最小的数)
输入输出格式
输入格式:
第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号( 1≤opt≤6 )
输出格式:
对于操作3,4,5,6每行输出一个数,表示对应答案
输入样例#1:
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
输出样例#1:
106465
84185
492737
说明:
时空限制:1000ms,128M
1.n的数据范围: n≤100000
2.每个数的数据范围: [ -{10}^7 , {10}^7 ]
###思维
这是树堆模板题,对于树堆,它有4种方式的操作
####一 、插入
给节点随机分配一个优先级,先和二叉排序树的插入一样,先把要插入的点插入到一个叶子上,然后跟维护堆一样,如果当前节点的优先级比根大就旋转,如果当前节点是根的左儿子就右旋如果当前节点是根的右儿子就左旋。
我们如果把插入写成递归形式的话,只需要在递归调用完成后判断是否满足堆性质,如果不满足就旋转,实现非常容易。
由于是旋转的二叉排序树,最多进行h次(h是树的高度),插入的复杂度是
l
o
g
n
log_n
logn的,在期望情况下,所以它的期望复杂度是 O(
l
o
g
n
log_n
logn )
####二 、删除
有了旋转的操作之后,Treap的删除比二叉排序树还要简单。因为Treap满足堆性质,所以我们只需要把要删除的节点旋转到叶节点上,然后直接删除就可以了。具体的方法就是每次找到优先级最大的儿子,向与其相反的方向旋转,直到那个节点被旋转到叶节点,然后直接删除。删除最多进行log( n )次旋转,期望复杂度是log( n )。
第二种删除方法:为保证效率,可以用普通二叉查找树的删除方法,找到节点的中序前缀,然后替换,删除,并使用非递归。虽然时间复杂度仍为log级别,但常数因子小了很多。
####三 、查找
根据Treap具有二叉搜索树的性质,可以快速查找所需节点。
时间复杂度: 期望复杂度是O(
l
o
g
n
log_n
logn)。
####四 、 旋转
这个主要是为了维护树堆左右大致平衡,防止一面过长的现象发生
###Treap(树堆)实现代码
#include <bits/stdc++.h>
using namespace std;
struct pppp{
int key,yx;//键堆,优先级(小)
int siz,num;//子树元素总数,当前节点元素总数
pppp *ch[2];//两个儿子所在地址
};
pppp* root=NULL;
int ans,n,f,x;
void Rotate(pppp* &p,bool f){//旋转
pppp *t=p->ch[f^1];
p->ch[f^1]=t->ch[f];//改变t的子树的位置
t->ch[f]=p;//将t旋转至p上方
p->siz=p->num;
if(p->ch[0]!=NULL)p->siz+=p->ch[0]->siz;
if(p->ch[1]!=NULL)p->siz+=p->ch[1]->siz;
t->siz=t->num;
if(t->ch[0]!=NULL)t->siz+=t->ch[0]->siz;
if(t->ch[1]!=NULL)t->siz+=t->ch[1]->siz;//维护节点信息,自底向上先算p再算t
p=t;//将旋转上去的t节点作为当前子树新的根节点,使用引用方式传递
}
void Insert(pppp* &p,int x){//插入
if(p==NULL){//如果为空节点,就新建一个节点
p=(pppp *)malloc(sizeof(pppp));
p->key=x;p->yx=rand();
p->siz=p->num=1;
p->ch[0]=p->ch[1]=NULL;
return ;
}
if(p->key==x){++p->siz;++p->num;return ;}//重复元素直接累加在相应节点下面
if(x<p->key){
Insert(p->ch[0],x);//递归加入左子树
if(p->ch[0]->yx < p->yx)Rotate(p,1);//若插入的节点优先级小于当前子树的根节点则将其旋转至根节点上方
else ++p->siz;//如果需要旋转,则当前子树的所有节点的信息都已维护完成;如果不需旋转,那么在将元素递归插入子树后,只有当前子树的根节点的信息还未更新,那么就将其更新即可
}
else{
Insert(p->ch[1],x);
if(p->ch[1]->yx<p->yx)Rotate(p,0);
else ++p->siz;//如果需要旋转,则当前子树的所有节点的信息都已维护完成;如果不需旋转,那么在将元素递归插入子树后,只有当前子树的根节点的信息还未更新,那么就将其更新即可
}
}
void Del(pppp* &p,int x){//删除
if(p==NULL)return ;//保证不删除空节点
if(x==p->key){
if(p->num>1){--p->siz;--p->num;return ;}//若元素重复次数大于1,则减少元素个数即可
else{//若元素重复次数不大于1,需要删除节点
if(p->ch[0]==NULL){pppp *t=p;p=p->ch[1];free(t);return;}//若左子树为空则直接用右子树替代当前节点
else if(p->ch[1]==NULL){pppp *t=p;p=p->ch[0];free(t);return;}//若右子树为空则直接用左子树替代当前节点
else{
if(p->ch[0]->yx < p->ch[1]->yx){Rotate(p,1);Del(p->ch[1],x);}//左子树优先级小于右子树优先级,将左子树向上旋转,作为当前树新的根节点,递归在右子树中删除
else{Rotate(p,0);Del(p->ch[0],x);}//右子树优先级小于左子树优先级,将右子树向上旋转,作为当前树新的根节点,递归在左子树中删除
--p->siz;
}
}
}
else{//当前节点不是要删除的元素
if(x < p->key)Del(p->ch[0],x);//递归在左子树中删除
else Del(p->ch[1],x);//递归在右子树中删除
--p->siz;//递归删除完成后,还剩当前根节点的信息未更新,则将其更新即可
}
}
int Kth(pppp* p,int x){//求第k大元素
int s=0;//记录左子树节点数量
if(p->ch[0]!=NULL)s=p->ch[0]->siz;
if(x<=s)return Kth(p->ch[0],x);//查询节点位于左子树内,则其在左子树的排名即是在当前树的排名
else if(x<=s+p->num)return p->key;//当前节点即为所求
else return Kth(p->ch[1],x-s-p->num);//查询节点位于右子树内,则其在右子树的排名为:当前树排名-左子树元素数-当前树根节点元素数
}
int Rank(pppp* p,int x){//求元素排名
int s=0;
if(p->ch[0]!=NULL)s=p->ch[0]->siz;
if(x<p->key)return Rank(p->ch[0],x); //查询节点位于左子树内,则其在当前树的排名即是在左子树的排名
else if(x==p->key)return s+1; //找到待查询节点,则其在当前树的排名为:左子树元素数+1
else return s+p->num+Rank(p->ch[1],x); //查询节点位于右子树内,则其在当前树的排名为:左子树元素数+当前树根节点元素数+其在右子树的排名
}
void Pre(pppp* p,int x){//求前驱
if(p==NULL)return; //递归边界防止RE
if(p->key<x){ans=p->key;Pre(p->ch[1],x);}//当前节点键值为可行解,保存可行解,并尝试寻找更优解
else Pre(p->ch[0],x); //当前节点键值不是可行解则回退
}
void Succ(pppp* p,int x){//求后继
if(p==NULL)return;
if(p->key>x){ans=p->key;Succ(p->ch[0],x);}
else Succ(p->ch[1],x);//同Pre
}
int main(){
srand(time(0));
scanf("%d",&n);
while(n--){
scanf("%d%d",&f,&x);
switch(f){
case 1:Insert(root,x);break;
case 2:Del(root,x);break;
case 3:printf("%d\n",Rank(root,x));break;
case 4:printf("%d\n",Kth(root,x));break;
case 5:{Pre(root,x);printf("%d\n",ans);break;}
case 6:{Succ(root,x);printf("%d\n",ans);break;}
}
}
return 0;
}
这里我们主要运用了指针来写树堆,存起来更加方便。。
(具体实现和如何实现以及每一步的意义已经在代码里标注)