[您有新的未分配科技点]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)

时间:2023-11-09 17:44:56

今天我们来学习一种新的数据结构:无旋treap。它和splay一样支持区间操作,和treap一样简单易懂,同时还支持可持久化。

无旋treap的节点定义和treap一样,都要同时满足树性质和堆性质,我们还是用rand()来实现平衡

而无旋treap与treap不同的地方,也是其核心,就是它不旋转用两个新的核心函数:merge函数(合并两棵子树)和split函数(分裂出某棵树的前k个节点,并且作为一棵树返回)

首先看merge函数,它是一个递归实现的过程,先看代码:

 Treap *merge(Treap *a,Treap *b)
{
if(a==null)return b;
if(b==null)return a;
pushdown(a);pushdown(b);
if(a->key < b->key)
{a->ch[]=merge(a->ch[],b);a->update();return a;}
else
{b->ch[]=merge(a,b->ch[]);b->update();return b;}
}

对于两棵子树a和b,我们可以实现把b树合并到a树中

在合并时,我们首先看他们的根节点谁的键值比较小(我维护的是一个小根堆),并且建立对应的父子关系。

又由于平衡树的中序遍历不变,我们又要把b插在a后面,维持一个确定的中序遍历,

所以我们应该一直把a作为merge函数的前一个参数,b作为后一个参数,这个顺序不能换.

这一个确定的顺序的重要性尤其体现在后续的区间操作中。刚开始的时候可以当板子背下来,但随着打题肯定会逐渐理解。

接下来我们介绍split函数,这也是一个递归实现的过程,还是先看代码:

 typedef pair<Treap*,Treap*> D;
D split(Treap *o,int k)
{
if(o==null) return D(null,null);
D y;pushdown(o);
if(o->ch[]->size>=k)
{y=split(o->ch[],k);o->ch[]=y.second;o->update();y.second=o;}
else
{y=split(o->ch[],k-o->ch[]->size-);o->ch[]=y.first;o->update();y.first=o;}
return y;
}

我们首先定义一个pair,这样做的好处是同时返回分裂出来的两棵树的根节点指针,我规定第一个是分离完成的树,第二个是剩下的原树。

然后考虑分离前k个的过程:如果o的左儿子有k个以上节点,我们显然应该去左儿子分离。

然后我们会得到分离完成的树和左儿子剩下的树,这时候把左儿子剩下的部分接回节点o,并把新的o作为分离o剩下的原树

如果左儿子节点个数不够,我们就去右儿子分离,过程是相似的,但略有不同,留给读者思考。

有了这两个函数,我们就可以用他们实现一些常用的操作了,比如:

insert=split+newnode+merge+merge

delete=split+split+merge(合并第一个split的first和第二个的second)

等等,其他操作也可以用类似的思路打出来。下面我们用一道例题实战一下。建议读者自己实现代码并充分思考后再核对标程。

3224: Tyvj 1728 普通平衡树

Time Limit: 10 Sec  Memory Limit: 128 MB

Description

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1. 插入x数
2. 删除x数(若有多个相同的数,因只删除一个)
3. 查询x数的排名(若有多个相同的数,因输出最小的排名)
4. 查询排名为x的数
5. 求x的前驱(前驱定义为小于x,且最大的数)
6. 求x的后继(后继定义为大于x,且最小的数)

Input

第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1<=opt<=6)

Output

对于操作3,4,5,6每行输出一个数,表示对应答案

Sample Input

10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

Sample Output

106465
84185
492737

HINT

1.n的数据范围:n<=100000
2.每个数的数据范围:[-2e9,2e9]
题解:
这道题本质上只比上面讲的基本操作多了两个函数:查询某个权值的排名和查询某个排名的权值
查询某个权值的排名很简单,在树中递归询问即可
而对于某个权值的排名,我们可以考虑split前k-1个节点,再对第一次split的second进行split,得到第k个节点,并且返回权值
前驱和后继只是上面这两个操作的简单变形,但稍微需要注意一下边界的处理。
代码见下:
 #include <cstdio>
#include <algorithm>
#include <cstring>
#include <ctime>
#include <cstdlib>
using namespace std;
const int maxn=,inf=0x7fffffff;
struct Treap
{
Treap* ch[];
int key,val,size;
Treap(int v)
{size=,val=v,key=rand();ch[]=ch[]=NULL;}
inline void tain()
{size=+(ch[]?ch[]->size:)+(ch[]?ch[]->size:);}
}*root;
typedef pair<Treap*,Treap*> D;
inline int size(Treap *o){return o?o->size:;}
Treap *Merge(Treap *a,Treap* b)
{
if(!a)return b;
if(!b)return a;
if(a->key < b->key)
{a->ch[]=Merge(a->ch[],b);a->tain();return a;}
else
{b->ch[]=Merge(a,b->ch[]);b->tain();return b;}
}
D Split(Treap *o,int k)
{
if(!o)return D(NULL,NULL);
D y;
if(size(o->ch[])>=k)
{y=Split(o->ch[],k);o->ch[]=y.second;o->tain();y.second=o;}
else
{y=Split(o->ch[],k-size(o->ch[])-);o->ch[]=y.first;o->tain();y.first=o;}
return y;
}
int Getkth(Treap *o,int v)
{
if(o==NULL)return ;
return(o->val>=v)?Getkth(o->ch[],v):Getkth(o->ch[],v)+size(o->ch[])+;
}
inline int Findkth(int k)
{
D x=Split(root,k-);
D y=Split(x.second,);
Treap *ans=y.first;
root=Merge(Merge(x.first,ans),y.second);
return ans!=NULL?ans->val:;
}
inline void Insert(int v)
{
int k=Getkth(root,v);
D x=Split(root,k);
Treap *o=new Treap(v);
root=Merge(Merge(x.first,o),x.second);
}
void Delete(int v)
{
int k=Getkth(root,v);
D x=Split(root,k);
D y=Split(x.second,);
root=Merge(x.first,y.second);
}
int main(){
int m,opt,x;scanf("%d",&m);
while(m--)
{
scanf("%d%d",&opt,&x);
switch(opt)
{
case :Insert(x);break;
case :Delete(x);break;
case :printf("%d\n",Getkth(root,x)+);break;
case :printf("%d\n",Findkth(x));break;
case :printf("%d\n",Findkth(Getkth(root,x)));break;
case :printf("%d\n",Findkth(Getkth(root,x+)+));break;
}
}
}