Treap平衡树
1 二叉树搜索树与平衡树
二叉搜索树满足性质:
节点p的左子树内所有的关键值都小于等于p的关键值,
节点p的右子树内所有的关键值都大于p的关键值。
二叉搜索树可以方便地查到节点p的排名,以及查排名为k的节点编号
问题在于,如果有序地输入一个序列,那树就会退化为一条链,树的深度变为$n$,每次的操作复杂度变为$O(n)$
对此,出现了二叉搜索树的优化——平衡树。平衡树可以通过一些操作使得树的深度稳定在$O(log n)$
其中的Treap有是一种优化方案,它会随机生成优先值,令以优先值满足小根堆heap的性质,其关键值为二叉搜索树tree的性质
Treap通过两个操作进行优化,spilt(分裂)和merge(合并)
2 Treap可实现操作与代码
2.0 有关数组的定义
对于每个节点,我们需要记录它四个信息,分别如下:
int val[NUM];//该点的数值 int wei[NUM];//随机生成的值,满足heap性质 int ch[NUM][3];//该节点的左右孩子 int size[NUM];//该节点的总子树大小(包括自己)
2.1 分裂操作:保证堆到左边的点的数值都小于等于分裂值,右边的点都大于分裂值
初始树:
当分裂值为3时,分裂的步骤如下:
第一步:找到当前的根:
由于节点$1$的数值为$5$,大于分裂值$3$,所以节点$1$及其右子树堆到右边去
第二步:由于上一步的根大于分裂值,所以来到了上一步根的左子树
来到了$2$号节点,由于$2$号节点的数值为$3 = 3$,所以其与其左子树一起堆到左边
第三步:由于上一步的根小于等于分裂值,所以来到了上一步根的右子树
找到了节点$5$,由于其值为$4$,大于分裂值$3$,所以将其堆到右边
将节点$5$接到左子树上
这样就分裂完毕,得到了两棵树
代码如下:
void split(int p,int v,int &x,int &y){ //p这棵树按照v分裂,x和y是下一层分裂后两棵树的树根 if( !p ){ //如果没有这棵树根 x = y = 0;//没有分裂后的两棵树 return; } if( v < val[p] ){ //如果节点p的数值比按照值大 y = p;//改了上一层的ch[p][0],把当前节点接在右堆的左子树 split( ch[p][0],v,x,ch[p][0]);//分裂左子树,右堆再接就接到当前节点的左子树上 }else{ x = p;//同上 split( ch[p][1],v,ch[p][1],y ); } pushup(p);//重新计算p的大小 }
2.2 合并:以wei[ ]数组按照heap的性质合并
如对于上图的结果,将两棵树合并的过程如下:
第一步:比较两树树根的$wei$值
其中节点$2$的$wei[2] = 10,wei[5] = 7$,所以新树的根为节点$1$,并且把其右子树也一起搞过去
第二步:比较 右堆的左子树 与 左堆的根 的$wei$的值
因为$wei[5] = 21,wei[2] = 10 -> $,节点$2$的值小,所以将左堆的根与其左子树一起接到新树的左子树上
第三步:再比较 左堆的右子树 与 右堆的左子树 的$wei$值
由于左堆已经没有了右子树,所以直接将右堆的$5$号点加进当前节点($2$号点)的右子树
代码如下:
int merge(int x,int y){ if( !x || !y ) return x|y;//如果有一棵子树缺失,返回另一棵子树 if( wei[x] < wei[y] ){ //左边的优先值小的话 ch[x][1] = merge( ch[x][1],y );//将左边的右子树与右边合并,左子树照常不管了 pushup(x);//重新计算左边的大小 return x;//左堆的根当做新树的当前的根 }else{ //右边优先值小 ch[y][0] = merge( x,ch[y][0] );//同理,将右边的左子树跟左堆 pushup(y); return y; } }
2.3 删除:将整个树按照三个类型,拆成三个部分,再合并
我们将可以这个树按照三个标准:“小于删除值”、“等于删除值”、“大于删除值”分为三棵树
三棵树的树根分别为$x$,$y$,$z$,那么我们就只需要删除$y$树的根节点啦
先上代码:
void del( int p ){ int x,y,z; split( rt,p,x,z ); split( x,p-1,x,y ); if( y ){ //如果存在我们要删除的这个元素 y = merge( ch[y][0],ch[y][1] ); //去掉根节点,也就是让这个树的左右子树合并 } x = merge( x,y ); rt = merge( x,z ); }
解释一下代码:
比如下面这个图,我们要删除$3$这个节点
先按照$3$拆第一步:
再按照$2$拆第二步
合并y树的两个儿子节点
合并x,y树:
再合并一下x,z树即可(不再模拟)
2.4 插入:将原来的树分为两部分,再与新节点合并
用于插入一个权值为$p$的节点
我们按照$p$将原来的树分裂,这时$x$中的节点都小于等于$p$,$y$中的节点都大于$p$
再将新节点与$x,y$两棵树合并即可
代码如下:
void add( int p ){ //插入节点p int x,y; split( rt,p,x,y ); x = merge( nownode(p),x ); rt = merge( x,y ); }
2.5 给数值求排名:类似搜索树,但不完全是
如果我们要查询数值为$p$的节点的排名,可以将树按照$p$分裂
然后答案就是$x$树的大小+1(加上自己才是最终排名)
代码如下:
int getrank( int p ){ //查询值为p的排名 int x,y; split( rt,p,x,y ); int rank = size[x]+1; rt = merge( x,y ); return ans; }
2.6 给排名求数值:完全就是二叉搜索树
不再解释
上代码:
int getval( int rank ){ int now = rt; while( now ){ if( size[ch[now][0]]+1 == rank ) return val[now]; if( size[ch[now][0]]+1 > rank ) now = ch[now][0];//如果排名大了,说明前面的人多,往左 else{ //如果排名小了,说明前面的少,往右 rank -= size[ ch[now][0] ] + 1; //往右走说明当前节点以及其左子树必然被包含 //往右走后树内将不再包含当前节点以及其左子树 //所以rank要减去当前节点及其左子树的大小 now = ch[now][1]; } } return INF;//没有查到目标排名,返回没有节点 }
2.7 求前驱后继
也基本就是二叉搜索树
分裂后一直往一个方向搜即可
代码:
int getfre( int p ){ int x,y; splite( rt,p-1,x,y ); int now = x; while( ch[now][1] ) now = ch[now][1]; ans = val[now]; rt = merge( x,y ); return ans; } int getnxt( int p ){ int x,y; splite( rt,p,x,y ); int now = y; while( ch[now][0] ) now = ch[now][0]; ans = val[now]; rt = merge( x,y ); return ans; }
3 总代码
有一些第一遍打出锅了的地方,要注意昂
#include<iostream> #include<stdlib.h> #include<cstdio> #include<time.h> #define NUM 1000010 #define INF 0x7f7f7f using namespace std; int wei[NUM],val[NUM],size[NUM]; int ch[NUM][3]; int cnt,n,rt; void pushup( int p ){ size[p] = size[ch[p][0]] + size[ch[p][1]] + 1;//不知为何某人总不写这几个字符,反而写return } void split( int p,int v,int &x,int &y ){ //** if( !p ){ x = y = 0; return; } if( v < val[p] ){ //如果当前节点大 y = p; split( ch[p][0],v,x,ch[p][0] ); }else{ x = p; split( ch[p][1],v,ch[p][1],y ); } pushup( p );//我让你不写这句话! } int merge( int x,int y ){ //** if( !x || !y ) return x|y;//if(!x|!y)是什么鬼啊! if( wei[x] > wei[y] ){ //执行的语句写反了,理清思路 ch[x][1] = merge( ch[x][1],y ); pushup( x ); return x; }else{ ch[y][0] = merge( x,ch[y][0] ); pushup( y ); return y; } } int newnode( int v ){ val[++cnt] = v; wei[cnt] = rand(); size[cnt] = 1; return cnt; } void add( int v ){ int x,y; int z = newnode( v ); split( rt,v,x,y ); x = merge( x,z ); rt = merge( x,y ); } void del( int v ){ int x,y,z; split( rt,v,x,z ); split( x,v-1,x,y ); if( y ) y = merge( ch[y][0],ch[y][1] ); x = merge( x,y ); rt = merge( x,z ); } int getrank( int v ){ //** int x,y; split( rt,v-1,x,y ); int ans = size[x]+1; rt = merge( x,y ); return ans; } int getval( int rank ){ int now = rt; while( now ){ if( size[ch[now][0]]+1 == rank ) //是比自己小的,if条件写错啦! return val[now]; if( size[ch[now][0]]+1 < rank ){ //这里也是错了 rank -= size[ch[now][0]]+1;//是左子树的大小! now = ch[now][1]; }else now = ch[now][0]; } } int getfre( int v ){ int x,y; split( rt,v-1,x,y ); int now = x; while( ch[now][1] ) now = ch[now][1]; int ans = val[now]; rt = merge( x,y ); return ans; } int getnxt( int v ){ int x,y; split( rt,v,x,y ); int ans,now = y; while( ch[now][0] ) now = ch[now][0]; ans = val[now]; rt = merge( x,y );//让你不写rt = return ans; } int main(){
srand( time(0) ); cin >> n; int op,v; while( n-- ){ cin >> op >> v; if( op == 1 ) add( v ); else if( op == 2 ) del( v ); else if( op == 3 ) cout << getrank( v ) << endl; else if( op == 4 ) cout << getval( v ) << endl; else if( op == 5 ) cout << getfre( v ) << endl; else if( op == 6 ) cout << getnxt( v ) << endl; }
return 0; }