数据结构可爱捏
1. 什么是非旋treap
非旋treap,本质上是一颗利用了随机数优化的二叉搜索树,可以通过合并(merge)操作和分离(split)操作的组合,实现点的添加与删除,查询排名、前驱、后继等操作,也可以实现很多线段树无法实现的区间操作,十分优秀,就是有点搞心态。(绝对不会告诉你我的饭卡套就是因此而牺牲的)
1.1. 为什么要随机数优化
显然,对于同样的一个集合,它可以生成出许多不同的二叉搜索树。而如果出题人是个毒瘤,那么,二叉搜索树会退化成链,时间复杂度为 O ( n ) O(n) O(n),而随机数优化后,它的高度的期望值为 log n \log n logn,即时间复杂度直接降为 O ( log n ) O(\log n) O(logn),相当优秀。
那么,随机数优化到底是怎样优化的?
显然,二叉搜索树的节点权值只有一种,而非旋treap的节点权值分为两种,一种是他自己所带的权值key,另一种则是随机数赋予它的rk
在创建非旋treap时,首先保证rk的值是小的在上,大的在下,在保证key值“左子树小于自己小于右子树”,这样一来,就相当将原数列随机打乱后再插入到一颗二叉搜索树中,就能实现随机数优化
简洁一点:玄学优化
2. merge操作
merge操作,就是将两颗非旋treap合并为一颗非旋treap
显然,考虑两颗树的根节点(假设分别是 a a a 和 b b b):谁的 rk 值小,谁就成为新的非旋treap的跟(这里假设 a a a 成为了根),然后,比较key值,如果 b b b 的key值小于 a a a 的key值,则 b b b 这颗非旋treap再与 a a a 的左子树合并,否则, b b b 这颗非旋treap与 a a a 的右子树合并
代码:
int merge(int root1,int root2){
//这里保证了key[root1]<key[root2 ]
if(!root1||!root2){//只剩一棵非旋treap就可以停止了
return root1+root2;
}
if(rk[root1]<rk[root2]){
son[root1][1]=merge(son[root1][1],root2);
return root1;
}else{
son[root2][0]=merge(root1,son[root2][0]);
return root2;
}
}
3. split操作
一般来说,split操作分为两种,一种是按照key值,一种是按照树的大小。这里以key值大小为例(假设为 y y y)
显然,如果根节点 a a a 的key值小于 y y y,那么,节点 a a a 本身和它的左子树全部分成左边部分,然后只需要分 a a a 的右子树即可;反之,节点 a a a 本身和它的右子树全部分成左边部分,然后只需要分 a a a 的左子树即可
代码:
void split(int root,int x,int &a1,int &a2){
if(!root){//分到尽头
a1=a2=0;
return ;
}
if(key[root]<=x){//两种情况
split(son[root][1],x,son[root][1],a2);
a1=root;
}else{
split(son[root][0],x,a1,son[root][0]);
a2=root;
}
}
基本上,非旋treap的核心就是这两个代码了,其它的操作基本上都是由这两个操作搭配而来
解决这道题,我们还需要维护一个Size
数组,Size
数组可以在merge操作和split操作中更新
想要解决后三个操作,我们需要再写一个函数:Find(int now,int x)
,意思是在根节点为now
的树中寻找第x
号节点。显然,若now
的左子树的Size小于等于
x
x
x,那就去左子树找;若now
的左子树的Size再加一等于
x
x
x,那么答案就是now
号节点;否则,就去右子树里面找
代码:
int find(int now,int k){
while(1){
if(Size[son[now][0]]>=k){
now=son[now][0];
}else if(Size[son[now][0]]+1==k){
return now;
}else{
k-=(Size[son[now][0]]+1);//记得去右子树时更新k值
now=son[now][1];
}
}
}
这样一来,这道题就可以迎刃而解了,代码:
#include<ctime>
#include<cstdio>
#include<cstdlib>
const int N=1000005;
int n;
int cnt,root;
int key[N],rk[N],son[N][2],Size[N];
void pushup(int x){
Size[x]=Size[son[x][0]]+Size[son[x][1]]+1;
}
int merge(int root1,int root2){
if(!root1||!root2){
return root1+root2;
}
if(rk[root1]<rk[root2]){
son[root1][1]=merge(son[root1][1],root2);
pushup(root1);
return root1;
}else{
son[root2][0]=merge(root1,son[root2][0]);
pushup(root2);
return root2;
}
}
void split(int root,int x,int &a1,int &a2){
if(!root){
a1=a2=0;
return ;
}
if(key[root]<=x){
split(son[root][1],x,son[root][1],a2);
a1=root;
}else{
split(son[root][0],x,a1,son[root][0]);
a2=root;
}
pushup(root);
}
int make_new(int x){
key[++cnt]=x,rk[cnt]=rand(),Size[cnt]=1;
return cnt;
}
int add(int root,int x){//加入新结点
int X,Y;
split(root,x,X,Y);
return merge(merge(X,make_new(x)),Y);
}
int del(int root,int x){//删掉节点
int X,Y,Z;
split(root,x,X,Y);
split(X,x-1,X,Z);
return merge(merge(X,merge(son[Z][0],son[Z][1])),Y);//注意,因为只要求删掉一个节点,如果直接将(X,Y)合并,会出问题
}
int find(int now,int k){
while(1){
if(Size[son[now][0]]>=k){
now=son[now][0];
}else if(Size[son[now][0]]+1==k){
return now;
}else{
k-=(Size[son[now][0]]+1);
now=son[now][1];
}
}
}
void solve_num_rank(int x){//根据数字查排名
int X,Y;
split(root,x-1,X,Y);
printf("%d\n",Size[X]+1);//左子树节点个数加上它本身
merge(X,Y);
}
void solve_rank_num(int x){//根据排名查数字
printf("%d\n",key[find(root,x)]);
}
void solve_pre(int x){//查询前驱
int X,Y;
split(root,x-1,X,Y);
printf("%d\n",key[find(X,Size[X])]);//左子树的最后一个
merge(X,Y);
}
void solve_next(int x){//查询后继
int X,Y;
split(root,x,X,Y);
printf("%d\n",key[find(Y,1)]);//右子树的第一个
merge(X,Y);
}
int main(){
srand(time(0));
scanf("%d",&n);
while(n--){
int op,x;
scanf("%d%d",&op,&x);
if(op==1){
root=add(root,x);
}else if(op==2){
root=del(root,x);
}else if(op==3){
solve_num_rank(x);
}else if(op==4){
solve_rank_num(x);
}else if(op==5){
solve_pre(x);
}else{
solve_next(x);
}
}
return 0;
}
这里随便举几个例题吧:
显然,只需要按顺序插入每一个数,然后查询它的前驱以及后继即可,注意判断这个数是否有前驱、后继
MIN_SORT_GAP
:显然,每插入一个数,他都会和它的前驱以及后继各产生一个差值,所以,我们每往treap1
插入一个数,就分别用它的前驱以及它的后继与该数字本身各产生一个差,用它们来更新最小值,再在该操作中输出这个最小值即可
INSERT
:假设我们插入的数字是
x
x
x,它前面的数是
a
a
a,后面的数是
b
b
b
显然,插入后,原本的
a
−
b
a-b
a−b 就没了,而变为了
x
−
a
x-a
x−a 和
b
−
x
b-x
b−x,分别将它们删除,加入treap2
即可
注意, b b b 有可能是不存在的,注意特判
MIN_GAP
:输出treap2
的第一位即可
这道题可以建两颗treap,但是我只建了一颗,因为显然,当宠物大于人时,treap里面装的时宠物的信息,反之,装的是人的信息
这里以宠物信息为例:
每有一个人加入时,查询前驱和后继,哪个与之的差小(差一样就选择前驱),就将答案累加给它,然后删除对应的点即可
我们还需要两个变量:sum,minn
,分别表示员工工资的起伏和假设员工工资不变,工资下限的相对起伏(有点抽象,但应该能理解)
为了解决这道题,我们还需要两个变量,分别表示公司里的人数和离开公司的人数
I
:先判断这个员工的工资是否小于工资下限,如果不小于,就将该员工加入即可
A
:调整sum
和minn
S
:调整sum
和minn
,同时,判断目前公司工资最小的员工是否跳槽,持续跳槽,直到无人跳槽(即查询排名第一小的员工)
F
:找到排名对应的数,注意加上sum
(特判:公司人数可能不足
k
k
k)
最后输出跳槽人数即可
还在咕咕咕呢喵