Treap思路
Treap性质
建一棵二叉搜索树,每个节点除了有一个数值外,还需要赋予一个随机值 r d rd rd。这一棵二叉搜索树同时满足两个条件:
- 满足二叉搜索树的基本性质:一个节点的左儿子的值小于他,一个节点的右儿子的值大于它。
- 赋予的随机值满足小顶堆的基本性质。
Treap复杂度
事实上,Treap至今还没有得到严谨的证明时间复杂度为 Θ ( n log n ) \Theta(n\log n) Θ(nlogn),但是只要这个随机生成的序列不是基本严格递增的,那么这一棵Treap就一定不会退化为 Θ ( n 2 ) \Theta(n^2) Θ(n2),接近于 Θ ( n log n ) \Theta(n\log n) Θ(nlogn)。虽然Treap的时间复杂度没有Splay树等稳定,但一般也不会卡点测试你的代码,srand成系统时间即可。
Treap旋转
改变Treap的形态但是不改变Treap性质的过程称之为旋转。Treap的旋转定义与Splay树有所不同。Treap中,左旋指把根节点放到左边,右旋指把根节点放到右边,可形象理解为左旋即为把整棵树向左边掰一个节点,右旋即为把整棵树向右边掰一个节点。
思路基本介绍完毕,接下来讲代码。
Treap基本代码
所需变量
开始前,需要定义两个基本变量: r t , t o t rt,tot rt,tot,其中, r t rt rt表示该Treap的根节点是谁,而 t o t tot tot表示一共有多少个节点。
对于树中的每一个节点,需要有这样几个变量: l , r , s z , c n t , k e y , r d l,r,sz,cnt,key,rd l,r,sz,cnt,key,rd。 l , r l,r l,r分别表示这个节点的左儿子与右儿子分别是谁, s z sz sz表示以这个点为根节点的二叉树的所有节点数量是多少, c n t cnt cnt表示与该节点相等的数有多少个(如果值相等,那么直接将该节点的 c n t cnt cnt++,但是不加入新的节点), k e y key key表示数值, r d rd rd表示赋予该节点的随机数。
c o d e : \tt{code:} code:
struct node{
int l,r,sz,cnt,key,rd;
}tr[100010];
int rt=0,tot=0;
加入节点
每次新加入节点时,需要通过点权( k e y key key值)大小找到该数值在Treap中的正确位置,将这个节点的 l , r l,r l,r都设为 0 0 0,将 s z , c n t sz,cnt sz,cnt都设为 1 1 1(都只有该节点自己), k e y key key值即为数值, r d rd rd值即为随机生成值。
在加入节点完毕后,回溯的时候需要将整一棵树左旋或右旋,保证平衡性。
c o d e : \tt{code:} code:
void insert(int &nd,int x){//插入
if(nd==0){
nd=++tot;
tr[nd].key=x;
tr[nd].rd=rand();
tr[nd].cnt=tr[nd].sz=1;
return;
}
tr[nd].sz++;
if(x==tr[nd].key){
tr[nd].cnt++;
}
else if(x<tr[nd].key){
insert(tr[nd].l,x);
if(tr[nd].rd>tr[tr[nd].l].rd){
Rr(nd);
}
}
else{
insert(tr[nd].r,x);
if(tr[nd].rd>tr[tr[nd].r].rd){
Lr(nd);
}
}
}
删除节点
首先找到该数值的位置,然后将该数值的节点的 r d rd rd值改为 i n f inf inf,然后不断按照规则左旋或者右旋。查找位置过程中不断维护树的 c n t , s z cnt,sz cnt,sz值,时时更新。最后旋完以后,要删除的节点一定会在整个Treap的最右边,最下方。将那个点象征性删除即可。
c o d e : \tt{code:} code:
void DEL(int &nd){//服务del
if(tr[nd].l==0||tr[nd].r==0){
nd=tr[nd].l+tr[nd].r;
return;
}
if(tr[tr[nd].l].rd<tr[tr[nd].r].rd){
Rr(nd);
DEL(tr[nd].r);
}
else{
Lr(nd);
DEL(tr[nd].l);
}
}
void del(int &nd,int x){//删除
if(nd==0){
return;
}
if(tr[nd].key==x){
tr[nd].sz--;
tr[nd].cnt--;
if(tr[nd].cnt==0){
DEL(nd);
}
}
else if(x<tr[nd].key){
del(tr[nd].l,x);
}
else if(x>tr[nd].key){
del(tr[nd].r,x);
}
tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
}
其中第一个DEL
函数是为del
函数服务的,调用时直接调用del
即可。
旋转
左旋与右旋原理基本相同,以右旋为例:
将根节点的左儿子定为根节点,将自己定位新的根节点的右儿子,将新的根节点的左儿子不变,新的根节点的右儿子变为原来根节点的左儿子(那个位置必定为空位)。
c o d e : \tt{code:} code:
void Rr(int &nd){//右旋
int w=tr[nd].l;
tr[nd].l=tr[w].r;
tr[w].r=nd;
tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
nd=w;
}
void Lr(int &nd){//左旋
int w=tr[nd].r;
tr[nd].r=tr[w].l;
tr[w].l=nd;
tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
nd=w;
}
前驱与后继
思路简单,以求前驱为例:
如果该节点是没有的,那么直接返回无穷大;如果该节点的值小于要找的值,那么返回该位置的值与右子树求该数前驱的较大值;如果该节点的值大于要找的值,那么返回从他的左子树找到的要找的数的前驱的值。思路描述有些拗口,需仔细理解。后继的求法与前驱类似,代码也类似。
c o d e : \tt{code:} code:
int pre(int nd,int x){//前驱
if(nd==0){
return INT_MIN;
}
if(tr[nd].key<x){
return max(tr[nd].key,pre(tr[nd].r,x));
}
else{
return pre(tr[nd].l,x);
}
}
int succ(int nd,int x){//后继
if(nd==0){
return INT_MAX;
}
if(tr[nd].key>x){
return min(tr[nd].key,succ(tr[nd].l,x));
}
else{
return succ(tr[nd].r,x);
}
}
查询
查询分为两种:查询数
x
x
x的排名与查询排名为
x
x
x的数。思路看代码应该就能看懂(个人认为应该属于Treap的衍生能力,不属于基本能力)。思路不再赘述,只是一堆。if
c o d e : \tt{code:} code:
int pm(int nd,int x){//数x的排名
if(nd==0){
return INT_MIN;
}
if(tr[nd].key==x){
return tr[tr[nd].l].sz;
}
if(tr[nd].key>x){
return pm(tr[nd].l,x);
}
else{
return tr[tr[nd].l].sz+tr[nd].cnt+pm(tr[nd].r,x);
}
}
int rk(int nd,int x){//排名为x的数
if(tr[tr[nd].l].sz>=x){
return rk(tr[nd].l,x);
}
if(tr[tr[nd].l].sz+tr[nd].cnt>=x){
return tr[nd].key;
}
return rk(tr[nd].r,x-tr[tr[nd].l].sz-tr[nd].cnt);
}
Treap例题
题目
思路
思路已经全部在上面介绍,这里不再赘述,Treap模板题。
代码
声明:仅供参考。
#include<bits/stdc++.h>
using namespace std;
struct node{
int l,r,sz,cnt,key,rd;
}tr[100010];
int rt=0,tot=0;
void Rr(int &nd){
int w=tr[nd].l;
tr[nd].l=tr[w].r;
tr[w].r=nd;
tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
nd=w;
}
void Lr(int &nd){
int w=tr[nd].r;
tr[nd].r=tr[w].l;
tr[w].l=nd;
tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
tr[w].sz=tr[tr[w].l].sz+tr[tr[w].r].sz+tr[w].cnt;
nd=w;
}
void DEL(int &nd){
if(tr[nd].l==0||tr[nd].r==0){
nd=tr[nd].l+tr[nd].r;
return;
}
if(tr[tr[nd].l].rd<tr[tr[nd].r].rd){
Rr(nd);
DEL(tr[nd].r);
}
else{
Lr(nd);
DEL(tr[nd].l);
}
}
void del(int &nd,int x){
if(nd==0){
return;
}
if(tr[nd].key==x){
tr[nd].sz--;
tr[nd].cnt--;
if(tr[nd].cnt==0){
DEL(nd);
}
}
else if(x<tr[nd].key){
del(tr[nd].l,x);
}
else if(x>tr[nd].key){
del(tr[nd].r,x);
}
tr[nd].sz=tr[tr[nd].l].sz+tr[tr[nd].r].sz+tr[nd].cnt;
}
void insert(int &nd,int x){
if(nd==0){
nd=++tot;
tr[nd].key=x;
tr[nd].rd=rand();
tr[nd].cnt=tr[nd].sz=1;
return;
}
tr[nd].sz++;
if(x==tr[nd].key){
tr[nd].cnt++;
}
else if(x<tr[nd].key){
insert(tr[nd].l,x);
if(tr[nd].rd>tr[tr[nd].l].rd){
Rr(nd);
}
}
else{
insert(tr[nd].r,x);
if(tr[nd].rd>tr[tr[nd].r].rd){
Lr(nd);
}
}
}
int pre(int nd,int x){
if(nd==0){
return INT_MIN;
}
if(tr[nd].key<x){
return max(tr[nd].key,pre(tr[nd].r,x));
}
else{
return pre(tr[nd].l,x);
}
}
int succ(int nd,int x){
if(nd==0){
return INT_MAX;
}
if(tr[nd].key>x){
return min(tr[nd].key,succ(tr[nd].l,x));
}
else{
return succ(tr[nd].r,x);
}
}
int pm(int nd,int x){
if(nd==0){
return INT_MIN;
}
if(tr[nd].key==x){
return tr[tr[nd].l].sz;
}
if(tr[nd].key>x){
return pm(tr[nd].l,x);
}
else{
return tr[tr[nd].l].sz+tr[nd].cnt+pm(tr[nd].r,x);
}
}
int rk(int nd,int x){
if(tr[tr[nd].l].sz>=x){
return rk(tr[nd].l,x);
}
if(tr[tr[nd].l].sz+tr[nd].cnt>=x){
return tr[nd].key;
}
return rk(tr[nd].r,x-tr[tr[nd].l].sz-tr[nd].cnt);
}
int main(){
int m,op,x;
srand(time(0));
cin>>m;
while(m--){
cin>>op;
cin>>x;
int t;
switch(op){
case 1:insert(rt,x);break;
case 2:del(rt,x);break;
case 3:t=pm(rt,x);cout<<(t<0?-1:t+1)<<endl;break;
case 4:if(tr[rt].sz<x)cout<<-1<<endl;else cout<<rk(rt,x)<<endl;break;
case 5:cout<<pre(rt,x)<<endl;break;
case 6:cout<<succ(rt,x)<<endl;break;
}
}
return 0;
}
Treap练习
推荐几道洛谷的高质量Treap题目,供练习。
鬼子进村
送花
全部是Treap模板,思路简单。