笔者一个数据结构的蒟蒻仍是奇迹般的搞明白了splay的基本原理以及实现方法,因此写下这篇随笔但愿能帮到像我当初一脸懵逼的人。ios
咱们从二叉查找树开始提及:数组
二叉查找树是一棵二叉树,它知足这样一个性质:全部小于当前节点的点都在该节点的左子树上,全部大于当前节点的点都在该节点的右子树上。对于和当前节点同样大的点,咱们有两种方法,一种是直接默认它到右子树上去,可是这样会形成空间的浪费。咱们有一种比较好的操做是设置一个权值数组,若是出现了这种同样的状况,就直接把这个点的权值+1就能够了。数据结构
手绘了一棵二叉查找树:函数
那么这棵树有什么用呢?工具
咱们先来看这样一道题吧:优化
很明显,这个题咱们能够直接用最高级的数据结构——数组实现,直接全读进来排个序什么的就直接OK了。ui
可是出题人就是想让这个题变得难一点,他使这个题变成了一边插入一边询问。很明显,刚才那个方法萎了,如今咱们就要引入咱们的二叉查找树了。spa
显然,咱们能够很轻松的使用二叉查找树来完成插入这个工做。重要的是完成询问2和3。3d
先来看询问2吧:因为二叉查找树的性质,咱们比较询问的妹子的好感度与当前节点的好感度,若是少了那就向左查找,多了就向右查找。咱们最终老是会找到的。而后这个妹子前面有k我的,那么这个妹子就排名为k+1.code
而后是询问3:咱们比较每一个节点的k值和当前的k值,依然按照二叉查找树的性质比较大小就能够了。
这样咱们就可指望O(nlogn)出解来八卦掉Refun大神。
可是题是死的,人是活的,出题人是毒瘤的,每种这样的题目总会有这样的数据:给出的插入彻底有序,结果咱们的两个查找一会儿全成了n的复杂度。
就想是这样一棵树:
你看这棵长坏了的树。那如何解决这种问题呢?天然是使这棵树平衡起来,具体实现有treap,splay等等。
如今咱们就引入今天的正题——splay
splay
首先声明一些变量:
和一些操做:
求当前节点是左?右?儿子:
inline int get(int x) { return ch[f[x]][1]==x; }
清零操做:
inline void clear(int x){ ch[x][0]=ch[x][1]=size[x]=f[x]=key[x]=cnt[x]=0; }
更新size值的操做:
inline void update(int x) { if(x){ size[x]=cnt[x]; if(ch[x][0]) size[x]+=size[ch[x][0]]; if(ch[x][1]) size[x]+=size[ch[x][1]]; } }
而后就是splay的关键操做了,旋转。
有人可能有疑问了,这旋转有个P用,看上去啥都没改变啊。然而实际上,这旋转就是成功把x向上提了一个位置,而咱们的目标就是像这样一步步把一个节点向上提到他的一个祖先下面,或者就这么变成了根。
那这个右旋应该怎么样实现呢?咱们分三步来解释:
一:咱们先看看x有没有右子树,若是有的话,让它成为y的左子树,同时让它认y作爹。
二:咱们看看,这个时候x就没有右子树了,咱们就让y认x作爹,而后让y做为x的右子树。
三:咱们再看看,y有没有爹,若是有的话,假定这个爹叫z,那么让x认z作爹,而且要与y的左右子树的性质一致。
贴一段代码,看看应该挺好理解的:
至于左旋和右旋很像,不过代码笔者仍是码了的:
至于实际操做的时候,咱们天然不能够把这俩玩意分开,实现起来很复杂,因此用ch数组的两维表明左右儿子,经过一个综合函数来实现这两个函数。而且在旋转完了以后要紧接着update维护一下。
这样咱们最基础的旋转就已经搞定了,接下来咱们要实现splay的关键操做,splay。
splay的目的在于把一个节点一直转到一个给定的节点底下,而后,通常人们都直接旋转到根。
能够用一个简短的代码归纳一下
至于怎么旋转,咱们要分状况讨论:
若是x,y,z三个点在同一个直线上的话,那么就要先旋转y,不然咱们就先旋转x。若是不这么作的话,就会形成树的失衡。
那么咱们能够先看一下繁杂的代码,不过好理解是真的:
很明显的是,这个代码很长,不过看上去应该仍是比较清楚的,下面提供一种简洁不少的版本:
对于直接旋转到根的状况来讲,这两个代码是彻底等价的。
而后就是依题目而定的具体操做了,这里咱们以各大OJ上都有的一道普通平衡树的模板题来示例。
首先看一下他须要让咱们进行的操做
那咱们就一步步的看这些操做都怎么实现吧:
1.插入一个数:都还记着笔者刚刚开始说二叉查找树的时候就已经说过了插入是一个很简单的工做了吧。。
(1):首先对于root==0时,明显树是空的,进行一些特殊操做直接退出来就好了。
(2):对于root!=0时的状况,若是在向下寻找的时候咱们寻找到了一个和它同样大的点,咱们就能够直接把它的权值加1,而后update维护下它和它的爹,再splay一下。
若是咱们直接找到了最底下,那没什么好说的了,把树的大小+1,因为它是最底下的节点,不必update本身,直接维护一下父节点,splay一下就行。
代码老是有的,笔者就是这么的善解人意:
删除一个数比较麻烦一会再说;
2.查找一个数的排名
这里的操做就和二叉查找树愈来愈像了。
(1):若是当前节点的数值比咱们如今的小,那么不用进行其余的任何操做,咱们直接继续向左子树查找就能够了。
(2):若是当前节点的数值比咱们如今的大,那么咱们就把返回值加上左子树以及根的大小,而后向右子树查找。
还有一个,找着了以后要splay一下。。
3.查询一个排名的数
(1):首先一上来先看看正找着的这个点有没有左子树,若是有的话,而且它的大小比x大,那么就向左查找,不然向右。
(2):向右查找的时候,注意把节点的大小和右子树的大小都记录下来,以便判断是否要继续向右子树查找。
3:求x前驱和后继
这个操做比较容易的吧,不过得想对。
对于这两个操做,咱们直接先插进去x,而后求出它在树上的前驱和后继,天然也就是它的前驱和后继,而后把它删掉就能够了。
而后咱们发现,在插入这个x的时候咱们把它旋转到了根节点的位置上,因此前驱就是它左子树最右的节点,就是先向左找一下,而后一直找到没有右儿子了为止,同理后继就是它右子树最左的节点。(不知道为何建议向上翻翻找着二叉查找树的定义仔细阅读)。
至于怎么找,不想说了,实在不明白的就看代码明白吧。。
5:删除操做
这个操做仍是比较麻烦的,注意的地方也教前面的操做多一点。
(1):为了方便接下来的操做,先把x旋转到根节点,随你怎么转过去。
(2):而后分状况讨论,如今x已是根节点,若是它的权值不为1,那就好办,-1以后返回就好了。
(3):然而确定有不少是1的,怎么办?若是x一个孩子都没有,把x删了就行,反正树上就它一个节点。
(4):若是x只有任何一个儿子,那么把x删了,直接让儿子当爹就行。
(5):若是有两个儿子的话,首先咱们要先选一个根,天然是x的前驱或后继,这里咱们选择前驱,而后把前驱旋转到根节点,而后再把x原来的右子树当作它的右子树,update维护一下就行。
这样一来,这个题就这么结束了。
其实splay整个操做都是基于二叉查找树的,咱们的rotate操做很明显是符合二叉查找树性质的。
看上去完了?
没有,咱们还要说一个点.
用splay实现区间翻转
其实,要操做起来有不少种能够用splay实现的方法了,这里介绍一种看上去正常实现起来比较容易的。
咱们根据二叉查找树的性质,能够看出假如咱们要在Splay中修改区间的话,能够先查找siz值为l与r+2的两个节点,将一个旋转到根,另外一个旋转到根的右儿子上,则要修改的区间就是根的右孩子的左子树,直接打标记便可。
为何这么旋转就能够?先上图:
理解一下,红圈里的两个点就是咱们要旋转的点,第二个图中蓝圈里的就是要翻转的区间,而且这样翻转完了以后它仍然与开始那个图的中序遍历相同。绿色的点就是咱们要翻转的点。。为何是这些点。。。由于要翻转的必定是比l的下标大比r+2下标小的点。
至于代码能够这么实现,不是一个很麻烦的事。
还有一个要说的是,咱们作这个题创建平衡树的时候,是按照数组下标建树,而不是按照大小建树。因此颇有必要放一下代码强调一下。
眼神好的人应该能看出来这份代码和下面的有些区别,事实上,这个代码可以一开始的时候创建出一个完美平衡树(虽然不久以后它就不那么完美了),理论上可以快一点吧。而下面的代码一开始颇有可能建出来,额。。。一条链,不过很快也会splay掉了。
放上题目的彻底代码了。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<algorithm> 6 #include<queue> 7 #define re register 8 #define maxn 1000007 9 #define ll long long 10 #define ls rt<<1 11 #define rs rt<<1|1 12 #define inf 1000000007 13 using namespace std; 14 int ch[100001][2],f[maxn],cnt[maxn],key[maxn],size[maxn],mark[maxn],root,sz,data[maxn]; 15 inline int pushdown(int x) 16 { 17 if(x&&mark[x]){ 18 mark[ch[x][0]]^=1; 19 mark[ch[x][1]]^=1; 20 swap(ch[x][0],ch[x][1]); 21 mark[x]=0; 22 } 23 } 24 inline void clear(int x) 25 { 26 ch[x][0]=ch[x][1]=f[x]=cnt[x]=key[x]=size[x]=0; 27 } 28 inline int get(int x) 29 { 30 return ch[f[x]][1]==x; 31 } 32 inline void update(int x) 33 { 34 size[x]=size[ch[x][1]]+size[ch[x][0]]+1; 35 } 36 inline void rotate(int x) 37 { 38 int y=f[x],z=f[y]; 39 int kind=get(x); 40 pushdown(y);pushdown(x); 41 ch[y][kind]=ch[x][kind^1];f[ch[y][kind]]=y; 42 ch[x][kind^1]=y; 43 f[y]=x; f[x]=z; 44 if(z){ 45 ch[z][ch[z][1]==y]=x; 46 } 47 update(y);update(x); 48 } 49 inline void splay(int x,int tar){ 50 for(re int fa;(fa=f[x])!=tar;rotate(x)) 51 if(f[fa]!=tar){ 52 rotate(get(x)==get(fa)?fa:x); 53 } 54 if(!tar) root=x; 55 } 56 inline int build(int fa,int l,int r) 57 { 58 if(l>r) return 0; 59 int mid=l+r>>1; 60 int now=++sz; 61 key[now]=data[mid],f[now]=fa,mark[now]=0; 62 ch[now][0]=build(now,l,mid-1); 63 ch[now][1]=build(now,mid+1,r); 64 update(now); 65 return now; 66 } 67 inline int findx(int k) 68 { 69 int now=root; 70 while(1) 71 { 72 pushdown(now); 73 if(k<=size[ch[now][0]]) 74 now=ch[now][0]; 75 else{ 76 k-=size[ch[now][0]]+1; 77 if(!k) return now; 78 now=ch[now][1]; 79 } 80 } 81 } 82 inline void print(int now) 83 { 84 pushdown(now); 85 if(ch[now][0]) print(ch[now][0]); 86 if(key[now]!=-inf && key[now]!=inf) 87 printf("%d ",key[now]); 88 if(ch[now][1]) print(ch[now][1]); 89 } 90 int main() 91 { 92 int n,m,x,y; 93 cin>>n>>m; 94 for(re int i=1;i<=n;i++) 95 { 96 data[i+1]=i; 97 } 98 data[1]=-inf;data[n+2]=inf; 99 root=build(0,1,n+2); 100 for(re int i=1;i<=m;i++) 101 { 102 cin>>x>>y; 103 int x1=findx(x),y1=findx(y+2); 104 splay(x1,0); 105 splay(y1,x1); 106 mark[ch[ch[root][1]][0]]^=1; 107 } 108 print(root); 109 }
其实splay更多的是一种辅助的工具,理解了以后代码难度略小于treap(由于笔者如今还没搞懂treap),并且灵活多变,能够处理多类问题,至于常数大这个缺点,用各类玄学方式优化一下吧。。。