「学习笔记」treap(小根堆)
总结教训
对于 treap 使用小根堆性质,一定要特判左右子树是否存在,因为空节点的优先级为 \(0\),是最高的,不特判会出错我就这么错了,So
一定要特判!一定要特判!一定要特判!重要的事情说三遍
本文代码根据P3369 【模板】普通平衡树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)写的
模板,有注释:
//treap(小根堆性质)
#include<bits/stdc++.h>
#define rint register int
typedef long long ll;
using namespace std;
inline ll read()//快读
{
ll x=0;
bool fg=false;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') fg=true;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return fg?~x+1:x;
}
const int N=1e6+5;
const ll INF=1e9+5;
int n,tot,root;//n 操作次数 tot 记录节点的标号 root 根节点
int ch[N][2];//左右孩子
ll val[N],pai[N],cnt[N],siz[N];
//val 数值 pai 存储优先级 cnt 该数值出现了几次 siz 大小
inline void pushup(int id)//更新siz
{
siz[id]=siz[ch[id][0]]+siz[ch[id][1]]+cnt[id];
//更新,该节点的大小=左子树的大小+右子树的大小+自己出现了几次
}
int New(ll v)//插入新节点
{
val[++tot]=v;
pai[tot]=rand();
//随机函数,随即赋予一个数值,头文件<stdlib.h>
cnt[tot]++;//cnt[tot]=1;
siz[tot]++;//siz[tot]=1;
return tot;
}
void spin(int &id,int d)
//旋转 id 要旋转的节点编号,因为会改变,所以要加& d(direction) 旋转方向
{
int temp=ch[id][d^1];//记录与旋转方向相反的子节点,这里画图自己理解一下
ch[id][d^1]=ch[temp][d];//先存下来
ch[temp][d]=id;//再修改
pushup(id);//先更新原节点
pushup(temp);//再更新旋转后的节点
id=temp;//最后更新id,以便其他操作需要
}
void build()
//初始化,这两个点一个在最左下角,一个在最右下角,这里最左下角的会影响后面有关排名的的查询
{
root=New(-INF);
ch[root][1]=New(INF);
if(pai[root]>pai[ch[root][1]]) spin(root,0);//不符合小根堆性质,就左旋
}
void insert(int &id,ll x)//插入节点
{
if(id==0)//如果已经到叶子节点了,直接插入新节点
{
id=New(x);
return;
}
if(x==val[id]) cnt[id]++;//如果和该节点数值相等,cnt(出现次数)+1
else
{
int d=x<val[id]?0:1;//判断是属于左子树还是右子树
insert(ch[id][d],x);
if(pai[ch[id][d]]<pai[id]) spin(id,d^1);//要符合小根堆的性质
}
pushup(id);//更新大小
}
void del(int &id,ll x)
{
if(!id) return;//如果没有这个点,直接返回
//这里else不能少,因为id是会改变的
if(x==val[id])//就是该节点,进行特判
{
if(cnt[id]>1) cnt[id]--;//出现多次,减一即可
else//这里小根堆一定要进行特判子树,因为空节点的优先级最高,会报错
{
if(ch[id][0]&&!ch[id][1])//只有左子树,没有右子树
spin(id,1),del(ch[id][1],x);
else
{
if(!ch[id][0]&&ch[id][1])//只有右子树,没有左子树
spin(id,0),del(ch[id][0],x);
else
{
if(ch[id][0]&&ch[id][1])//左右子树都有
{
int d=pai[ch[id][0]]<pai[ch[id][1]]?1:0;//判断优先级
spin(id,d);//旋转
del(ch[id][d],x);//删除
}
else id=0;//左右子树都没有
}
}
}
}
else//如果不是该节点,向子树出发
{
int d=x<val[id]?0:1;
del(ch[id][d],x);
}
pushup(id);//更新大小
}
ll get_rank(int id,ll x)//找x数(这里是数字)的排名(注意,是排名)
{
if(!id) return 0;//如果没有这个数,返回0
if(x<val[id]) return get_rank(ch[id][0],x);
//比当前节点小,向左子树中找
if(x>val[id]) return siz[ch[id][0]]+cnt[id]+get_rank(ch[id][1],x);
//比当前节点大,向右子树中找
return siz[ch[id][0]]+1;
//不大不小,则就是该节点,要返回这个点左子树的大小,并加上自己(也就是+1)
}
ll get_val(int id,ll x)//找排名为x的数(注意,是数字)
{
if(!id) return 0;//如果该节点为空,返回0
if(x<=siz[ch[id][0]]) return get_val(ch[id][0],x);
//如果排名小于等于左子树的大小,就说明排名的这个位置在左子树中
if(x>siz[ch[id][0]]&&x<=siz[ch[id][0]]+cnt[id]) return val[id];
//如果大于左子树大小,小于等于左子树大小+当前节点的大小,则就是该节点
return get_val(ch[id][1],x-siz[ch[id][0]]-cnt[id]);
//以上都不符合,就是在右子树,这里x要减去左子树大小和当前节点的出现次数
}
ll get_pre(ll x)//求前驱
{
int id=root;
ll pre;
while(id)
{
if(val[id]<x)//如果当前节点比x小,记录答案,去右子树中搜更优值
{
pre=val[id];//记录答案
id=ch[id][1];//id转到右子树
}
else id=ch[id][0];//否则,在左子树中搜
}
return pre;
}
ll get_nxt(ll x)//求后继
{
int id=root;
ll nxt;
while(id)
{
if(val[id]>x)//如果当前节点比x大,记录答案,去左子树中搜更优值
{
nxt=val[id];//记录答案
id=ch[id][0];//id转到左子树
}
else id=ch[id][1];//否则,在右子树中搜
}
return nxt;
}
int main()
{
build();//初始化
n=read();//读入操作次数
for(rint i=1;i<=n;++i)
{
int op=read();
ll x=read();
switch(op)
{
case 1:
insert(root,x);
break;
case 2:
del(root,x);
break;
case 3:
printf("%lld\n",get_rank(root,x)-1);
break;
case 4:
printf("%lld\n",get_val(root,x+1));
break;
case 5:
printf("%lld\n",get_pre(x));
break;
case 6:
printf("%lld\n",get_nxt(x));
break;
}
}
return 0;
}
大根堆模板链接:treap(大根堆)模板
朝气蓬勃 后生可畏