题目
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1.插入 xx 数
2.删除 xx 数(若有多个相同的数,因只删除一个)
3.查询 xx 数的排名(排名定义为比当前数小的数的个数 +1+1 。若有多个相同的数,因输出最小的排名)
4.查询排名为 xx 的数
5.求 xx 的前驱(前驱定义为小于 xx ,且最大的数)
6.求 xx 的后继(后继定义为大于 xx ,且最小的数)
题解
啊,平衡树!
表示蒟蒻的我只会(刚学)treap,于是就模板了一下最基础的那种treap(通过旋转操作维持平衡)
treap是什么呢,emmmm,就是一棵二叉查找树,然后每个点有一个随机出来的key值,对树上的点用key值维护一个大根(小根)堆。这样的一棵树,期望高度是log(n),所以期望的复杂度也是log(n)。
有6种操作
1.insert插入一个数
2.dele删除一个数(若有多个相同的数,因只删除一个)
3.rank查询 xx 数的排名(排名定义为比当前数小的数的个数+1 。若有多个相同的数,因输出最小的排名)
4.val查询排名为 xx 的数
5.pre求 xx 的前驱(前驱定义为小于 xx ,且最大的数)
6.next求 xx 的后继(后继定义为大于 xx ,且最小的数)
这些操作对于我来说是如此精妙,以至于代码里打满注释来让自己理解记忆
对于旋转操作,有
左旋 根结点变左儿子,右儿子变根节点,右儿子的左儿子变左儿子的右儿子
右旋 根结点变右儿子,左儿子变根节点,左儿子的右儿子变右儿子的左儿子
代码
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstdlib>//随机数的头文件
using namespace std;
struct node{
int l,r;
int key,data,size;
}t[100005];
int n,root,cnt;
const int m=100000000;
void updata(int x){
t[x].size=t[t[x].r].size+t[t[x].l].size+1;
}//更新size值,在插入、删除、换位置时要用
void rttn(int &x)
{
int y=t[x].l;
t[x].l=t[y].r;
t[y].r=x;
//根结点变右儿子,左儿子变根节点,左儿子的右儿子变右儿子的左儿子
updata(x);updata(y);//换位置后更新size值
x=y;//x值会被赋值给记录整棵树根节点的root,而y是右旋后的根节点
}//传说中的右旋
void lttn(int &x)
{
int y=t[x].r;
t[x].r=t[y].l;
t[y].l=x;
updata(x);updata(y);
x=y;
}//同上
void insert(int &x,int k){//插入
if (x==0){
x=++cnt;
t[cnt].key=rand();
//key值为随机数,用堆维护后可防止树退化
t[cnt].data=k;
t[cnt].size=1;
return;
}
//一直找到可以插入k值的叶子节点,此时x=0,执行插入操作
if (k<=t[x].data){
insert(t[x].l,k);//往左子树找
if (t[x].key>t[t[x].l].key) rttn(x);
} else {
insert(t[x].r,k);//往右子树找
if (t[x].key>t[t[x].r].key) lttn(x);
}
updata(x);//记得插入后要更新size值
}
void dele(int &x,int k){//删除
具体步骤:先找到要删除的数
如果这个数在叶子节点上就直接删除
删除就是把它的父亲连向该节点的t[x].l或t[x].r赋值为0
如果只有左儿子,就右旋,把要删除的数移到右儿子,递归删除右儿子
如果只有右儿子操作同上
如果左右儿子都有,就根据key值决定左旋或右旋,决定的依据
是保持大根(小根)堆性质,即保持key值大(小)的为根节点
if (t[x].data==k){//找到要删除的数了
if (t[x].l || t[x].r){
if (t[x].l&&((!t[x].r) || (t[t[x].l].key<t[t[x].r].key))){
rttn(x);
dele(t[x].r,k);
updata(x);//删除后要更新size值
} else {
lttn(x);
dele(t[x].l,k);
updata(x);//删除后要更新size值
}
} else x=0;
return;
}
if (k<t[x].data) dele(t[x].l,k); else dele(t[x].r,k);
//寻找要删除的数
updata(x);//删除后要更新size值
}
int rank(int x,int k){
if (x==0) return 0;
if (t[x].data>=k) return rank(t[x].l,k);
else return rank(t[x].r,k)+t[t[x].l].size+1;
}
如果x大于当前子树的根,那么说明左儿子及树根都比x小,答案
加上Lson.size+1,并且往右儿子走;否则往左儿子走,不统计
答案。
int val(int x,int k){
if (t[t[x].l].size+1==k) return t[x].data;
if (k<t[t[x].l].size+1) return val(t[x].l,k); else
return val(t[x].r,k-t[t[x].l].size-1);
}
我们给每个节点记录一个size,表示以该节点为根的子树大小。
根据Treap性质,一个点在它的子树内的rank其实就是左子树的
大小+1,也就是说,如果K=Leftson.size+1,当前点即为答案,
如果K<Leftson.size+1,则往左儿子搜索第K大,否则往右儿子
搜索第K-(Lson.size+1)大。
int pre(int k){//前驱,比x小的数里最大的一个
int ans=-m,x=root;
while (x){
if (k>t[x].data) ans=max(t[x].data,ans),x=t[x].r;
else x=t[x].l;
//虽然树中各点的大小是按顺序的,但是ans还是比较一下再赋值比较稳妥
}
return ans;
}
如果x大于当前子树的根,说明当前节点的值有可能作为答案,
去更新ans,然后往右儿子走;不然当前值不能更新答案,
往左儿子走。
int next(int k){//后继,同上,类似于前驱
int ans=m,x=root;
while (x){
if (k<t[x].data) ans=min(ans,t[x].data),x=t[x].l;
else x=t[x].r;
}
return ans;
}
int main(){
scanf("%d",&n);
for (int i=1;i<=n;i++){
int p,x;
scanf("%d%d",&p,&x);
if (p==1) insert(root,x);
if (p==2) dele(root,x);
if (p==3) printf("%d\n",rank(root,x)+1);
if (p==4) printf("%d\n",val(root,x));
if (p==5) printf("%d\n",pre(x));
if (p==6) printf("%d\n",next(x));
}
}