最近学习了一下无旋Treap,发现无旋Treap真的太好打了,而且也很好理解,很好用。
Treap?
什么是Treap?
顾名思义:Treap=Tree+heap,即“树堆”,“树”指的是二叉查找树,“堆”就是堆。
那二叉查找树跟堆有什么关系呢?
当输入的数据十分恶心的时候,普通的二叉查找树的时间复杂度就会由 n l o g 2 n nlog_2n nlog2n 退化到 n 2 n^2 n2 级别,时间复杂度难以让人接受。
而我们现在考虑优化,我们可以在每一个节点上多放一个数,然后使得这棵树既满足二叉排序树的性质也满足堆的性质,那么就可以尽可能地避免极端数据的影响。
有的人要问了,那这个数怎么求啊?
很简单,随机数。但C++的rand()函数太慢,因此我们也可以手打,毕竟也不怕题人卡。
如何维护?
一般的Treap是通过旋转来保持这个性质,但是这样打常数很大并且代码量也很大,不大好打。
于是我们便引入了无旋Treap。
无旋Treap
顾名思义,不用旋转的Treap叫做无旋Treap。
那我们怎样才能维护呢?
先说明指针是什么意思:
struct Treap{
int ls,rs;//左右儿子
int id;//随机数
int val;//该节点的值
int size;//该节点的子树大小
}T[1000005];
那就要引入Treap的两个核心操作:split(分裂) 和 merge(合并)
这两个操作到底有啥用呢,待会儿就知道了。
Split
分裂一般来说有两种方法:
- 按照数值分成两颗Treap,一颗上面的值全部小于等于(或大于等于)val,另一颗相反。
- 按照size来分裂
一般来说是采取第一种方法,在此也重点将这一种方法。
那我们怎么分裂呢?
当我们遇到了一个节点,我们判断,假若这个节点的val<=需要分裂的val,那么我们把这个节点以及它的左子树放到a里面,然后往右递归。否则相反。
是不是很简单?
//更新
void update(int p){
T[p].size=T[T[p].ls].size+T[T[p].rs].size+1;
return;
}
//分裂
void split(int p,int &x,int &y,int val){
if (p==0){
x=y=0;
return;
}
if (T[p].val<=val){
x=p;
split(T[p].rs,T[x].rs,y,val);
}
else{
y=p;
split(T[p].ls,x,T[y].ls,val);
}
update(p