Treap旋平衡树
平衡树(Treap==tree+heap)
二叉搜索树固然好用,但是容易退化成一条链,复杂度会被卡,那么怎样去掉这个缺点呢,聪明的人类有办法了。
其实不难发现,二叉搜索树在保证有序的前提下有很多形状,譬如:
和这玩意
很明显是等价的嘛,而且后者复杂度明显是低的,那我们就要想办法尽可能构造出下面这棵树而不是上面那棵。
那咋办嘞?
回想一下搜索树的定义
struct tree{
int l,r,v,sum,size;
}t[MAXN];
现在我们的插入操作是按照 \(v\) 大小一个个往里放的,给你个有序序列你就完蛋。。。
那么就有个人想出了一个好办法,我改变一下搜索树的定义,我在原来的基础上加点东西:
struct tree{
int l,r,v,sum,size;
int ran;
}t[MAXN];
\(ran\) 存一个随机值,要知道,我们这是一棵二叉树啊!!所以是可以实现堆操作的。
我们现在不光维护树中的各个 \(v\) 有序,我们还存了一个堆在里面,这样让树同时维护一个有序序列和一个堆,这样形状就不会是链了。
刚才讲了 \(ran\) 是随机值,我们在对 \(n\) 个数排序的同时,又生成了 \(n\) 个随机值并对其排序,那你说树还退化成链的可能吗?
没有!!
那么这就是平衡树的原理了,\(Treap=Tree+heap\)
那么接下来就是如何维护的问题了,我们知道维护一个堆,其实本质就是让父亲和儿子互相交换。
该上来的上来,该下去的下去。
那么如何交换父子节点还能让交换前后的平衡树等价呢?
上图:
有问题吗?没有问题。美其名曰:左右旋。。。
如果左儿子上来就右旋,右儿子上来就左旋。
用字母表示一下:
发现其实只有 \(A、B\) 点改变了。
那么假设我们对节点\(g\)进行旋转,也就是:
右旋:\(g\)的左儿子变成\(g\)的左儿子的右儿子。\(g\)的左儿子的右儿子变成\(g\);
左旋:\(g\)的右儿子变成\(g\)的右儿子的左儿子。\(g\)的右儿子的左儿子变成\(g\);
void R_rotate(int &g){
int y=t[g].l;
t[g].l=t[y].r;
t[y].r=g;
push_up(g);push_up(y);
g=y;
}//右旋
void L_rotate(int &g){
int y=t[g].r;
t[g].r=t[y].l;
t[y].l=g;
push_up(g);push_up(y);
g=y;
}//左旋
一定要传实参!!(不要问为啥,写多了就悟出来了)
那么剩下的就和搜索树基本是一回事儿了,只不过多了个删除:
板子题面自己看
准备操作:
int n,opt,v,rt,head_size;
struct tree{
int l,r,v,sum,size,ran;
}t[100005];
void push_up(int &g){t[g].size=t[t[g].l].size+t[t[g].r].size+t[g].sum;}//维护子树大小
void R_rotate(int &g){
int y=t[g].l;
t[g].l=t[y].r;t[y].r=g;
push_up(g);push_up(y);
g=y;
}//右旋
void L_rotate(int &g){
int y=t[g].l;
t[g].l=t[y].r;t[y].r=g;
push_up(g);push_up(y);
g=y;
}//左旋
\(1.\)插入:
void insert(int &g,int &v){
if(!g){
g=++head_size;
t[g].v=v;++t[g].sum;++t[g].size;
t[g].ran=rand();//随机产生一个数
return ;
}//如果当前节点为空,新增节点。
else { ++t[g].size;//来了一个点,子树大小+1;
if(t[g].v==v) ++t[g].sum;//如果相等直接sum+1
else if(v<t[g].v){//小于往左走
insert(t[g].l,v);
if(t[g].ran>t[t[g].l].ran)rxuan(g);//维护堆
}
else {//大于往右走
insert(t[g].r,v);
if(t[g].ran>t[t[g].r].ran)lxuan(g);//维护堆
}
}
push_up(g);//子树们一直在转,有必要维护当前节点的size;
}
\(2.\)删除\(v\)
如果是叶子,直接删除,没有问题。
如果在链上,直接让唯一的孩子代替自己,没有问题。
左右孩子都有,那就让它往下旋,直到变成以上两个情况为止。
void delet(int &g,int &v){
if(t[g].v==v){//找到点了
if(t[g].sum>1){--t[g].sum;--t[g].size;}//如果有重复的,那就直接--sum;
else if(!t[g].l||!t[g].r)g=t[g].l+t[g].r;//满足前两种情况,
else if(t[t[g].l].ran<t[t[g].r].ran){rxuan(g);delet(g,v);}//维护堆并对其进行相应左右旋
else {lxuan(g);delet(g,v);}
return ;
}
--t[g].size;//还没找到点的时候,说明要删的点肯定在在子树里,所以维护一下各个点的子树大小。
if(t[g].v<v)delet(t[g].r,v);//大了往右
else delet(t[g].l,v);//小了往左
}
\(3.\)查询\(v\)的排名
int ranking(int &v){
int g=rt,k=0;
while(g){
if(t[g].v==v)return k+t[t[g].l].size+1;
if(v<t[g].v)g=t[g].l;
else {k+=t[t[g].l].size+t[g].sum;g=t[g].r;}
}
return k;
}
自己悟……
\(4.\)查询排名为\(v\)的数
int find(int &v){
int g=rt,k=0;
while(g){
if(k+t[t[g].l].size>=v)g=t[g].l;
else {
k+=t[t[g].l].size;
if(k+t[g].sum>=v)return t[g].v;
else {k+=t[g].sum;g=t[g].r;}
}
}
}
自己悟……
\(5.\)求\(v\)的前驱
int front(int &v){
int g=rt,re=-200000000;
while(g){
if(t[g].v<v){re=t[g].v;g=t[g].r;}
else g=t[g].l;
}
return re;
}
自己悟……
\(6.\)求\(v\)的后继
int back(int &v){
int g=rt,re=200000000;
while(g){
if(t[g].v>v){re=t[g].v;g=t[g].l;}
else g=t[g].r;
}
return re;
}
自己悟……
还是说一定要传实参!!!(\(自己悟\))
然后平衡树你就会了。
事实上,会了搜索树就会平衡树。
无非就是加了个旋转,删除,挺简单的,嗯(挺简单还调两天)。
不过还挺好用!!!(虽然板子就一百行)
更好用的还得看FHQ