splay:优雅的区间暴力!

时间:2022-06-16 16:41:25

万年不更的blog主更新啦!主要是最近实在忙,好不容易才从划水做题的时间中抽出一段时间来写这篇blog

首先声明:这篇blog写的肯定会很基础。。。因为身为一个蒟蒻深知在茫茫大海中找到一个自己完全能够看懂的blog有多么的难。。(说多了都是泪。)所以当然希望所有初学者都能看懂这篇博文啦~

说实话在学这个算法之前有跟强大的巨神zxyer学过treap和fhq_treap,所以对平衡树有一定的了解。当然都是理论阶段,虽然都打过一两题,但是忘得快。。所以几乎等于没打。

认真重学了一遍平衡树(尤其是splay,一是好用,二来是为接下来我要讲的link-cut-tree做基础(万年坑))

蓦然发现。。妙不可言。。

平衡树这个东西,本身就是靠旋转保证复杂度的。

其实在讲splay的基础之前,我想先提出一个概念,或者说个人感觉。splay,其实就是一颗会动的线段树,他在线段树的基础上,加上了区间翻转,区间插入,区间删除等等线段树做不到的操作,虽然常数大,但是在同样是nlogn的复杂度面前,splay显然更华丽高级。

首先我们讲讲bst(binary_search_tree,中文为二叉搜索树)

如图即为一颗二叉搜索树

splay:优雅的区间暴力!

这棵树最基本的性质就是对于任何一个节点x,他的左子树的点的权值都<val[x],他的右子树的点的权值都>val[x]

对的。。这显然嘛

然后我们来讲一下这种树一般用来解决两个问题:

1、动态求整个序列(或者说插入元素的集合)中第k大的数

2、动态求权值val在树中的排名

在讲如何解决这两个问题之前,先介绍一下bst的基本操作:

一、插入

假设我们当前到达一个节点x,那么我们分类讨论v与当前节点的权值关系:

1、v<=val[x],递归进入x的左子树

2、v>val[x],递归进入x的右子树

如此循环,直到找到一个空节点,把该节点安插进去。

二、查找第k大

同上分类讨论k与当前节点的子树大小关系:

1、k<=sz[x.lson],递归进入x的左子树

2、k>sz[x.lson]+num[x](num[x]表示权值为val[x]的点的个数,一般为1),递归进入x的右子树

3、如果都不满足,则返回当前节点

三、查找v的排名

同上分类讨论v与当前节点的权值关系:

1、v<=val[x],递归进入x的左子树

2、v>val[x],递归进入x的右子树,返回答案时加上sz[x.lson]+num[x]


讲完基本操作,显然上面两个问题都是小菜一碟了

我们能够轻松归纳出复杂度,如果树的情况是一条链,显然最坏复杂度为n^2

这个复杂度是不能被大多数题目所接受的。这时就要推出splay重要的操作:旋转!旋转如图:

splay:优雅的区间暴力!

下面贴上右旋的伪代码(左旋同理,只不过lson变为rson而已)

{

  设f为当前节点的父亲,g为f的父亲

  fa[x]=g  fa[f]=x  fa[x.rson]=f

  g.rson==f?g.rson:g.lson=x  f.lson=x.rson  x.rson=f

}

可以看出,经过旋转之后,bst的性质是不变的。

具体证明可将节点权值关系变为不等式严格证明,在此就不多阐述了

那么为什么旋转能够优化复杂度呢?

显然我们知道一颗n个节点的满二叉树的深度是logn的,所以我们希望在插入一个节点或进行某种操作后,用同样操作复杂度为o(深度)的操作使树深度变得更平均。

根据某套证明splay复杂度的理论,可以得出,每次在查找到一个与答案的节点后把它旋转到根就能将树维持在一个接近满二叉树的形态,使得操作的复杂度变为nlogn。

在我看来,其实每次操作做完之后没事就乱旋旋,反正都是nlogn

在此我们直接跳过看上去比较玄学的单旋splay,进入比较科学的双旋splay

在此先说几个简写的类型

LL型:表示对于要旋转的节点x,fa[x].lson=x,fa[fa[x]].lson=fa[x]

RR型:表示对于要旋转的节点x,fa[x].rson=x,fa[fa[x]].rson=fa[x]

LR、RL型:以此类推

下面介绍对应这四种情况的对应方法。

在此先注明(图片转载至某巨神的blog http://blog.csdn.net/u014634338/article/details/49586689 其实是蒟蒻偷懒)

splay:优雅的区间暴力!

splay:优雅的区间暴力!splay:优雅的区间暴力!

splay:优雅的区间暴力!

相信上面的图已经非常详尽了。。配合我上面旋转的伪代码。可以好好理解一下。

然后我们做一个总结:

对于LR型和RL型,我们对当前节点x做了两次旋转

对于LL型和RR型,我们对当前节点的父亲做一次旋转,再对当前节点做一次旋转。

这有助于后期的代码简化。

不过双旋要注意一个事情,那就是只有在当前节点有祖父的情况下才能进行双旋。

所以我们就能搞出伪代码:

while(当前节点不是要旋到的节点){

  如果当前节点有祖父:rotate(LL型或RR型?x:fa[x]);

  如果当前节点不是要旋到的节点(由于在经过一次旋转后x的位置改变所以要重新判断,同时如果上一个操作没有执行那么本次操作为单旋)rotate(x);

}


讲完旋转后,我们讲讲splay基本操作:

1、插入:同bst插入,只不过在插入之后把新建节点旋到根即可

2、删除:分类讨论:

如果当前节点个数>1那么我们将个数-1,返回

如果当前节点只有一个子树,那么我们用这个子树的根替换我们要删除的节点

否则我们把我们要删除的节点旋到根,把该节点的后继旋到根的右儿子,显然该节点的后继的左子树为空(后继表示整棵树中第一个比当前节点权值大的点),我们把根的右儿子变为根,并将它的左子树重连即可。

其实还有很多操作。。不过blog主认为你们都会(比如求前驱后继什么的),blog主偷个懒啦,毕竟熬夜写blog也不太好嘛

讲完这两个基本操作,我们再讲讲splay最精髓的部分:区间操作!

splay区间操作的基本思想就是对于一个要操作的区间(l,r),将排名为l-1的节点旋到根节点,r+1的节点旋到根的右儿子,那么显然r+1的左子树就是整个区间。

对于这个区间我们可以通过修改标记等多种操作实现区间修改,区间求和,区间翻转等操作。

这个过程很简单,如果不懂具体实现详见下面的代码0.0

不过在看代码之前,首先要搞懂区间修改的原理。此时splay维护的不再是一颗权值bst,而是一个以数组下标为bst的数据结构了。所以这不是维护权值,而是维护下标!再说3遍!(维护下标!维护下标!维护下标!)只有了解了这个东西,你才能懂区间修改

在贴出代码前我讲讲几个实现的难点:

第一:插入的当前节点的处理:对于递归下传的过程中,不知道当前点为父亲的左儿子还是右儿子。

对于这个问题,有很多解决办法,例如传引用等等,不过我的splay由于写的是数组版,所以为了防止常数过大,我的ins函数传的是三个参数:父亲,为父亲的左儿子还是右儿子,插入点权值

第二,对于旋转代码过于冗长的问题。这个在我的代码中得到了很大的优化。通过判断当前节点是父节点的左儿子还是右儿子,可以将左旋和右旋压在一起,节省代码长度。

第三,没法查找[1,n]的区间的问题。对于这个问题,我们单独插入0和n+1号节点,这样就能查找啦0.0

其实splay还有很多玄妙的地方可供学习,不过本人时间有限。。只能写这么多啦。(后面会继续补坑的)(区间版splay我后期会补上的)

下面贴上板子。。各种操作都有啦(原题传送门

#include<cstdio>
#include<cstring>
#include<algorithm>
#define MN 400005
#define rtf 400004
#define rt c[rtf][0]
using namespace std;
int n,sz[MN],c[MN][],tn,fa[MN],val[MN],sum[MN],cnt;
void update(int x){sz[x]=sz[c[x][]]+sz[c[x][]]+sum[x];}
void rotate(int x){
int f=fa[x],ff=fa[f],l=c[f][]==x,r=l^;
fa[f]=x,fa[x]=ff,fa[c[x][r]]=f;
c[ff][c[ff][]==f]=x,c[f][l]=c[x][r],c[x][r]=f;update(f);
}
void splay(int x,int y){
for(int f;fa[x]!=y;rotate(x))
if(fa[f=fa[x]]!=y)rotate(c[fa[f]][]==f^c[f][]==x?x:f);
update(x);
}
void ins(int f,int ty,int v){
if(!c[f][ty]){fa[c[f][ty]=++tn]=f;val[tn]=v;sum[tn]=;splay(tn,rtf);return;}
if(v==val[c[f][ty]]){sum[c[f][ty]]++;splay(c[f][ty],rtf);return;}
++sz[f=c[f][ty]];
ins(f,v>val[f],v);
}
void del(int f,int ty,int v){
if(val[c[f][ty]]==v){
int x=c[f][ty];
if(sum[x]>){sum[x]--,splay(x,rtf);return;}int tmp;
if(!(c[x][]*c[x][])){c[f][ty]=c[x][]+c[x][],fa[c[x][c[x][]!=]]=f;return;}
for(tmp=c[x][];c[tmp][]!=;tmp=c[tmp][]);
splay(x,rtf),splay(tmp,rt);
c[tmp][]=c[x][],rt=tmp,fa[c[x][]]=tmp,fa[tmp]=rtf;update(tmp);return;
}--sz[f=c[f][ty]];del(f,v>val[f],v);
}
int findk(int x,int k){
if(k<=sz[c[x][]])return findk(c[x][],k);
else if(k>sz[c[x][]]&&k<=sz[c[x][]]+sum[x]){splay(x,rtf);return val[x];}
else return findk(c[x][],k-=sz[c[x][]]+sum[x]);
}
int getk(int x,int k){
if(!x)return ;
if(val[x]>=k)return getk(c[x][],k);
else return sz[c[x][]]+sum[x]+getk(c[x][],k);
}
int pre(int x){int tmp=getk(rt,x);return findk(rt,tmp);}
int suf(int x){int tmp=getk(rt,x+)+;return findk(rt,tmp);}
int main(){
scanf("%d",&n);int opt,x;
for(int i=;i<=n;i++){
scanf("%d%d",&opt,&x);
if(opt==)ins(rtf,,x);
else if(opt==)del(rtf,,x);
else if(opt==)printf("%d\n",getk(rt,x)+);
else if(opt==)printf("%d\n",findk(rt,x));
else if(opt==)printf("%d\n",pre(x));
else if(opt==)printf("%d\n",suf(x));
}
}