非旋Treap
好东西!!!从疯狂‘赚’的Treap到不‘赚’,Treap付出了时间的代价,但减少了代码的长度多么重要的一点呀!!!感谢神FHQ(orz)发明了这个数据结构。看过冗长的Treap后,再来打非旋Treap,是如此的释然。非旋Treap最大的优点就是代码短,方便实现,支持操作多。今天,来讲讲一种二分查找树非旋Treap。
非旋Treap最重要的操作是split(分裂)与merge(合并),其他的操作都是围绕着它展开的。
接下来,我们来讨论一下它的各项操作。
前导--笛卡尔树
一种建树方法
只是介绍一下。
- 笛卡尔树是一棵二叉树,树的每个节点有两个值,一个为 key,一个为 val。
- 光看 key 的话,笛卡尔树是一棵二叉搜索树,每个节点的左子树的 key 都比它 小,右子树都比它大;
- 光看 val 的话,笛卡尔树有点类似堆,根节点的 val 是最小(或者最大)的,每个节点的 val 都比它的子树要大。
- 笛卡尔树构造是和 Treap 完全一样的,如果 key 值是有序的,那么笛卡尔树的 构造是线性的,所以我们只要把 Treap 当作一颗笛卡尔树构造就可以了。
非旋Treap属于笛卡尔树,是通过笛卡尔树构造的。
非旋Treap
构造
笔者用的是指针。
struct Treap
{
Treap* ch[2];
int siz,rnd,val;
void updata(){
siz=1+ch[0]->siz+ch[1]->siz;
}
};
Treap* null=new Treap();
Treap* rt=null;
typedef pair<Treap*,Treap*> ptt;
分裂
无旋Treap的重点就在于它的有序性,它可以在找到操作位置后,将这棵树分裂成两棵有序的子树。因为非旋Treap具有二叉树搜索树的性质,我们可以通过对其儿子大小的判断,找到最合适的拆分点,对其进行拆分。
ptt split(Treap* p,int x)//分裂
{
if(p==null)
return ptt(null,null);
ptt y;
if(p->ch[0]->siz>=x){
y=split(p->ch[0],x);
p->ch[0]=y.second;
p->updata();
y.second=p;
return y;
}
y=split(p->ch[1],x-1-p->ch[0]->siz);
p->ch[1]=y.first;p->updata();y.first=p;
return y;
}
合并
在加入子树之后,我们会具有多棵树,此时,我们需要将它们合并在一起,所以,就出现了合并的操作,这样我们就可在过程中顺便维护一下非旋Treap的性质。注意在合并的过程中,要注意左右子树的有序性。
Treap* merge(Treap* a,Treap* b)//合并
{
if(a==null) return b;
if(b==null) return a;
if(a->rnd<b->rnd){
a->ch[1]=merge(a->ch[1],b);
a->updata();
return a;
}
b->ch[0]=merge(a,b->ch[0]);
b->updata();
return b;
}
再有了这些操作后,我们就有了下面的操作。
加点
通过分裂与合并,我们很轻松就实现了加点的操作。
void add(int d)//加点
{
int k=rankth(rt,d);
ptt x=split(rt,k);
Treap* p=new Treap();
p->ch[0]=p->ch[1]=null;p->siz=1;
p->rnd=rand();p->val=d;p->updata();
rt=merge(x.first,merge(p,x.second));
return ;
}
删点
通过分裂与合并,删点也很容易实现。
void del(int d)//删点
{
int k=rankth(rt,d);
ptt x=split(rt,k);
ptt y=split(x.second,1);
rt=merge(x.first,y.second);
}
查询排名
其实与Treap的打法也差不多,并没有什么大的差异。
int rankth(Treap* p,int x)//查询排名
{
if(p==null) return 0;
if(p->val>=x) return rankth(p->ch[0],x);
return rankth(p->ch[1],x)+1+p->ch[0]->siz;
}
查询第k小
查询的话与Treap有一些区别,分裂合并一趟就可以了。
int kth(int k)//查询第k小
{
ptt x=split(rt,k-1),y=split(x.second,1);
Treap* p=y.first;
rt=merge(x.first,merge(p,y.second));
if(p==null) return 0;
return p->val;
}
例题
照样可以交一下普通平衡树那道题,毕竟大部分平衡树都可以交。
#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXN 80005
using namespace std;
typedef long long LL;
const int INF=0x7f7f7f7f;
struct Treap
{
Treap* ch[2];
int siz,rnd,val;
void updata(){
siz=1+ch[0]->siz+ch[1]->siz;
}
};
Treap* null=new Treap();
Treap* rt=null;
typedef pair<Treap*,Treap*> ptt;
Treap* merge(Treap* a,Treap* b)//合并
{
if(a==null) return b;
if(b==null) return a;
if(a->rnd<b->rnd){
a->ch[1]=merge(a->ch[1],b);
a->updata();
return a;
}
b->ch[0]=merge(a,b->ch[0]);
b->updata();
return b;
}
ptt split(Treap* p,int x)//分裂
{
if(p==null)
return ptt(null,null);
ptt y;
if(p->ch[0]->siz>=x){
y=split(p->ch[0],x);
p->ch[0]=y.second;
p->updata();
y.second=p;
return y;
}
y=split(p->ch[1],x-1-p->ch[0]->siz);
p->ch[1]=y.first;p->updata();y.first=p;
return y;
}
int rankth(Treap* p,int x)//查询排名
{
if(p==null) return 0;
if(p->val>=x) return rankth(p->ch[0],x);
return rankth(p->ch[1],x)+1+p->ch[0]->siz;
}
int kth(int k)//查询第k小
{
ptt x=split(rt,k-1),y=split(x.second,1);
Treap* p=y.first;
rt=merge(x.first,merge(p,y.second));
if(p==null) return 0;
return p->val;
}
void add(int d)//加点
{
int k=rankth(rt,d);
ptt x=split(rt,k);
Treap* p=new Treap();
p->ch[0]=p->ch[1]=null;p->siz=1;
p->rnd=rand();p->val=d;p->updata();
rt=merge(x.first,merge(p,x.second));
return ;
}
void del(int d)//删点
{
int k=rankth(rt,d);
ptt x=split(rt,k);
ptt y=split(x.second,1);
rt=merge(x.first,y.second);
}
int n;
signed main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int opt,x;
scanf("%d %d",&opt,&x);
if(opt==1) add(x);
if(opt==2) del(x);
if(opt==3) printf("%d\n",rankth(rt,x)+1);
if(opt==4) printf("%d\n",kth(x));
if(opt==5) printf("%d\n",kth(rankth(rt,x)));
if(opt==6) printf("%d\n",kth(rankth(rt,x+1)+1));
}
return 0;
}
可持久化
- Build可以用来O(n)构树还可以在替罪羊树套Treap暴力重构的时候降低一个log 的复杂度。
- Merge和Split可用提取区间,因此可以操作一系列区间操作。
- Newnode单独拿出来很必要,这样在可持久化的时候会很轻松。
可持久化是对数据结构的一种操作,即保留历史信息,使得在后面可以调用之前 的历史版本。
- 由于只有父亲指向儿子的关系,所以我们可以在线段树进入修改的时候把沿途所 有节点都copy一遍。
- 然后把需要修改的指向儿子的指针修改一遍就好了,因为每次都是在原途上覆盖, 不会修改前一次的信息。
- 由于每次只会copy一条路径,而我们知道线段树的树高是log的,所以时空复 杂度都是nlog(n)。
- 如果带旋转,那么就会破环原有的父子关系,破环原有的路径和树形态,这是可 持久化无法接受的。
- 如果把Treap变为非旋转的,那么我们可以发现只要可以可持久化Merge和Split 就可一完成可持久化。
因为上文说到了‘一切可支持操作都可以通过以上四个基本操作完成’,而 Build操作只用于建造无需理会,Newnode就是用来可持久化的工具。
我们来观察一下Merge和Split,我们会发现它们都是由上而下的操作!
因此我们完全可以参考线段树的可持久化对它进行可持久化。
每次需要修改一个节点,就Newnode出来继续做就可以了。