文章目录
前言
S p l a y Splay Splay可以说是一个常数挺优秀的一个支持区间操作的平衡树,神奇的是在随机数据的情况下,有时候他能跑得玄学一般的快,这也取决于他复杂度玄学的证明方法,当然,他的 O ( m l o g n ) O(mlogn) O(mlogn)复杂度并非上限,而是均分,也就是可以卡。而我学他也只是单纯的用于LCT,因为在LCT中,他是能做到使得LCT的复杂度为 m l o g n mlogn mlogn的我所认知的唯一一颗平衡树。
当然,单点操作我也是用 S p l a y Splay Splay,毕竟 F H Q FHQ FHQ真的在单点操作的时候特别容易出锅,但同时FHQ的区间修改又比较强悍,代码短不容易出错,都是 F H Q FHQ FHQ的好处,而且支持可持久化。
真正的大佬应该再学一个 S B T SBT SBT,单点修改跑的飞快且代码听说也挺短的,我们机房就有个大佬是这样的,不过他不喜欢 S p l a y Splay Splay, L C T LCT LCT也不打 S p l a y Splay Splay,Orz。
Splay
Splay是什么,简单来说就是把一个点暴力跳到根节点来以此暴力玄学达到 O ( n l o g n ) O(nlogn) O(nlogn)的复杂度,听起来很玄学,实际上还是挺玄学好用的。
例题
时间限制: 2 Sec 内存限制: 128 MB
【题意】
写一种数据结构,来维护一些数,其中需要提供以下操作:
1. 插入x数
2. 删除x数(若有多个相同的数,应只删除一个)
3. 查询x数的排名(若有多个相同的数,应输出最小的排名)
4. 查询排名为x的数
5. 求x的前驱(前驱定义为小于x,且最大的数)
6. 求x的后继(后继定义为大于x,且最小的数)
注意:数据保证有解,无需特判
【输入格式】
第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号。
(1<=opt<=6,n < = 100000, 所有数字均在-10^7到10^7内 )
【输出格式】
对于操作3,4,5,6每行输出一个数,表示对应答案
【样例输入】
8
1 10
1 20
1 30
3 20
4 2
2 10
5 25
6 -1
【样例输出】
2
20
20
20
学习Splay
我们考虑一个类似堆得数据结构,堆不是要求儿子节点比父亲节点大(小)就行了吗,那么平衡树就是要求左字数所有节点的关键字小于父亲节点,右字数的节点大于父亲节点,也就是说这棵树的中序遍历是按关键字从小到大排序的,很明显,关键字就是决定树的形态的一个值。
这就是一棵较为典型的平衡树的样子。
而平衡树的复杂度取决于平衡树的层数,层数越大,时间越慢。
因为Splay其实可以说得上是用了大量的翻转来实现平衡的一棵树,所以我们现在先学习翻转。
翻转
总所周知,许多树的中序遍历都是一样的,也就是说连一个序列,Splay的样子也是可能有变化的,那么我们就来学一学如何利用一棵现有的Splay转换成其他形态的Splay。
我们仍然以上张图为例:
这就是一个经典的左旋右旋的例子。
很明显是一个 O ( 1 ) O(1) O(1)的操作。
而旋一个点的话,就是看他在父亲的那个儿子,在左儿子就右旋,在右儿子就左旋。
inline void update(int x){
tr[x].c/*子树内点的个数*/=tr[tr[x].son[0]].c+tr[tr[x].son[1]].c+tr[x].n/*与x号点点权相同的有多少个点*/;}
inline int chk(int x){
return tr[tr[x].f].son[1]==x;}//判断自己是什么儿子
inline void rotate(int x)
{
int f=tr[x].f,ff=tr[f].f,w=chk(x)^1/*0为左旋,1为右旋*/,son=tr[x].son[w];
int r,R;
r=son;R=f;tr[r].f=R;tr[R].son[w^1]=r;
r=x;R=ff;tr[r].f=R;tr[R].son[chk(f)]=r;
r=f;R=x;tr[r].f=R;tr[R].son[w]=r;
update(f);update(x);
}
splay
Splay的核心操作splay,QMQ。
就是把 x x x一直旋,直到 y y y为 x x x的父亲,不过要求 y y y一定是 x x x的祖先,那么分几种情况:
- x x x为 f a x fa_{x} fax的左(右)儿子, f a x fa_{x} fax为 f a f a x fa_{fa_{x}} fafax的左(右)儿子,那么就先旋 f a x fa_{x} fax,再旋 x x x。
- x x x为 f a x fa_{x} fax的右(左)儿子, f a x fa_{x} fax为 f a f a x fa_{fa_{x}} fafax的左(右)儿子,那么就旋 x x x两次。
- 如果 y = f a f a x y=fa_{fa_{x}} y=fafax,那么旋一下 x x x就行了。
很多人会不理解一操作,其实这是某大佬实验得出这样会快!
一操作的图(以一条链为例子,都是关键字为 1 1 1的点旋到 0 0 0的儿子,也就是根节点):
先旋父亲,再旋儿子:
连旋两次:
我们发现,一个层数为 4 4 4,一个层数为 5 5 5,所以先旋父亲,再旋儿子会使得Splay更平衡,其实我们可以这么想,旋转一个节点,相当于把父亲节点的东西装进我这个节点中,那么如果把一些点装进目前的父亲,一些点装进自己里面,不就会使得树更平衡了吗?
但是二操作为什么又不敢这样了呢。
二操作的情况但是先旋父亲,再旋儿子(以一条链为例子,关键字为 1 1 1的点旋到 0 0 0的儿子,也就是根节点):
会发现这个节点在旋了两次的情况下,只上了一层,而且理论上将树的层数不会更优,如下图,
我们会发现不管父亲节点怎么旋转, 4 4 4的层数就是不变,且树的总体层数也不变,也就是说其实旋父亲是没意义的。
但是又有人问为什么不只旋一次,有时候只旋一次以后就会出现操作一了,这样不会更优吗?
确实可能会更优,但是这是牺牲了一次判断的结果,而且最多就多一层或两层,使得慢得更慢,快得更快,而我们寻求的是一个综合性,所以旋两次也莫得多大问题。
那么这样不就好起来了吗?
inline void splay(int x,int fa)
{
while(tr[x].f!=fa)
{
int f=tr[x].f,ff=tr[f].f;
if(ff==fa)rotate(x);//操作三
else rotate(chk(x)^chk(f)?x:f),rotate(x);//三目运算符缩减操作一与操作二。
}
if(!fa)root=x;
}
求x的排名
这不是很简单吗,跳Splay不就行了?如果跳右子树,答案就加上左子树的树的个数。
然后把找到的点splay上去。
inline int findip(int d)//找到大于d且最接近d的节点或者小于d且最接近d
{
int x=root;
while(tr[x].d!=d)
{
if(!tr[x].son[tr[x].d<d])break;
x=tr[x].son[tr[x].d<d];
}
return x;
}
inline int findpaiming(int d)//这种方法仅限于存在这个数字的时候可以这么用
{
int x=findip(d);splay(x,0);
return tr[tr[x].son[0]].c+1;
}
提一提,findip不一定会找到最接近 d d d的点,但是能保证不会有点权是卡在 d d d与 t r [ x ] . d tr[x].d tr[x].d之间的。
求排名为x的数字
一样子,跳一跳,跳右子树就将 x x x减去左子树点的个数。
然后把找到的点splay上去。
inline int findkey(int d)//暴力往下跳
{
int x=root;
while(1)
{
if(d<=tr[tr[x].son[0]].c)x=tr[x].son[0];
else if(d<=tr[tr[x].son[0]].c+tr[x].n)break;
else d-=tr[tr[x].son[0]].c+tr[x].n,x=tr[x].son[1];
}
splay(x,0);//别忘splay维护形态
return tr[x].d;
}
前驱后继
找到最接近 d d d的点,并且splay,看看是不是前驱(后继),是就return,不是的话就往左(右)子树跳,因为是根节点,所以不需要考虑根的情况。
另外一种求法在可持久化FHQ中会写到。
inline int prep(int d)//后继
{
int x=findip(d);splay(x,0);
if(d<=tr[x].d && tr[x].son[0])
{
//因为是最接近d的值,且把相同的点权压到了一个点上,所以不可能存在小于等于tr[x].d且大于等于d的情况,所以左儿子肯定是小于d的
x=tr[x].son[0];while(tr[x].son[1])x=tr[x].son[1];
}
if(d<=tr[x].d)x=0;
return tr[x].d;
}
inline int qian(int d)//前驱
{
int x=findip(d);splay(x,0);
if(d>=tr[x].d && tr[x].son[1])
{
//同理
x=tr[x].son[1];while(tr[x].son[0])x=tr[x].son[0];
}
if(d>=tr[x].d)x=0;
return tr[x].d;
}
插点
首先,如果我们找的到一个点权和新插点的点权相等,那么直接 c , n + + c,n++ c,n++都可以,但是如果找不到的话,那么就找到最接近的点,直接添加为这个点的儿子,考虑最接近的点为 x x x,新加点的点权为 d d d的话,那么 t r [ x ] . d < d tr[x].d<d tr[x].d<d的话, d d d就会添加到右儿子,右儿子有没有可能有数字?有的话findip不就跳了吗,那可不可能跳左儿子? t r [ x ] . d tr[x].d tr[x].d都已经小于 d d d了,不会跳左儿子啦。
而 t r [ x ] . d > d tr[x].d>d tr[x].d>d也可以以此类推,也就是说findip一下不就可以了吗。
然后记得把新点或者相同点权的点splay一下
inline void ins(int d)
{
if(!root){
add(d,0);root=len;return ;}//没有点
int x=findip(d);
if(tr[x].d==d)tr[x].n++,update(x),splay(x,0);//相同点权
else add(d,x),update(x),splay(len,0);
}
删点
这又是一个麻烦的操作,首先我们仍然findip然后splay一下。
这个点就到了根节点,如果这个点的左子树或右子树为空的话, r o o t root root直接更新就行了。
但是不是的话,就会特别的麻烦,我们需要找到左子树最大的点,也就是前驱,然后把他旋到左子树的根,那么左儿子的右子树不就没了吗,那么就可以把 r o o t root root的右子树挪过去,然后取消左儿子与 r o o t root root的关联就可以了。
inline void del(int d)
{
int x=findip(d);splay(x,0);
if(tr[x].n!=1)tr[x].n--,tr[x].c--;
else if(!tr[x].son[0] && !tr[x].son[1])root=0;
else if(!tr[x].son[0] && tr[x].son[1]){
root=tr[x].son[1];tr[root].f=0;}
else if(tr[x].son[0] && !tr[x].son[1]){
root=tr[x].son[0];tr[root].f=0;}//前面都是各种简单的情况
else
{
int p=tr[x].son[0];while(tr[p].son[1])p=tr[p].son[1];//前驱
splay(p,x);//splay上去
tr[p].f=0;tr[p].son[1]=tr[x].son[1];update(p);
tr[tr[x].son[1]].f=p;
root=p;//让p当根节点
}
}
代码
#include<cstdio>
#include<cstring>
#define N 110000
using namespace std;
int root;
struct node
{
int d,n,f,c,son[2];
}tr[N];int len;
inline int chk(int x){
return tr[tr[x].f].son[1]==x;}
inline void update(int x){
tr[x].c=tr[tr[x].son[0]].c+tr[tr[x].son[1]].c+tr[x].n;}
inline void add(int d,int f)
{
len++;
tr[len].c=tr[len].n=1;tr[len].d=d;tr[len].f=f;
tr[f].son[d>tr[f].d]=len;
}
inline void rotate(int x)//翻转
{
int f=tr[x].f,ff=tr[f].f,w=chk(x)^1,son=tr[x].son[w];
int r,R;
r=son;R=f;tr[r].f=R;tr[R].son[w^1]=r;
r=x;R=ff;tr[r].f=R;tr[R].son[chk(f)]=r;
r=f;R=x;tr[r].f=R;tr[R].son[w]=r;
update(f);update(x);
}
inline void splay(int x,int fa)//splay
{
while(tr[x].f!=fa)
{
int f=tr[x].f,ff=tr[f].f;
if(ff==fa)rotate(x);
else rotate(chk(x)^chk(f)?x:f),rotate(x);
}
if(!fa)root=x;
}
inline int findip(int d)
{
int x=root;
while(tr[x].d!=d)
{
if(!tr[x].son[tr[x].d<d])break;
x=tr[x].son[tr[x].d<d];
}
return x;
}
inline void ins(int d)
{
if(!root){
add(d,0);root=len;return ;}
int x=findip(d);
if(tr[x].d==d)tr[x].n++,update(x),splay(x,0);
else add(d,x),update(x),splay(len,0);
}
inline void del(int d)
{
int x=findip(d);splay(x,0);
if(tr[x].n!=1)tr[x].n--,tr[x].c--;
else if(!tr[x].son[0] && !tr[x].son[1])root=0;
else if(!tr[x].son[0] && tr[x].son[1]){
root=tr[x].son[1];tr[root].f=0;}
else if(tr[x].son[0] && !tr[x].son[1]){
root=tr[x].son[0];tr[root].f=0;}
else
{
int p=tr[x].son[0];while(tr[p].son[1])p=tr[p].son[1];
splay(p,x);
tr[p].f=0;tr[p].son[1]=tr[x].son[1];update(p);
tr[tr[x].son[1]].f=p;
root=p;
}
}
inline int findpaiming(int d)
{
int x=findip(d);splay(x,0);
return tr[tr[x].son[0]].c+1;
}
inline int findkey(int d)
{
int x=root;
while(1)
{
if(d<=tr[tr[x].son[0]].c)x=tr[x].son[0];
else if(d<=tr[tr[x].son[0]].c+tr[x].n)break;
else d-=tr[tr[x].son[0]].c+tr[x].n,x=tr[x].son[1];
}
splay(x,0);
return tr[x].d;
}
inline int prep(int d)
{
int x=findip(d);splay(x,0);
if(d<=tr[x].d && tr[x].son[0])
{
x=tr[x].son[0];while(tr[x].son[1])x=tr[x].son[1];
}
if(d<=tr[x].d)x=0;
return tr[x].d;
}
inline int qian(int d)
{
int x=findip(d);splay(x,0);
if(d>=tr[x].d && tr[x].son[1])
{
x=tr[x].son[1];while(tr[x].son[0])x=tr[x].son[0];
}
if(d>=tr[x].d)x=0;
return tr[x].d;
}
int n;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int x,y;scanf("%d%d",&x,&y);
if(x==1)ins(y);
else if(x==2)del(y);
else if(x==3)printf("%d\n",findpaiming(y));
else if(x==4)printf("%d\n",findkey(y));
else if(x==5)printf("%d\n",prep(y));
else printf("%d\n",qian(y));
}
return 0;
}
区间操作
区间操作我都是用FHQ的,但是也提一提。
区间操作也是同样打标记,然后在splay的时候从下往上传标记,或者在其他地方。
下传标记要看情况直接修改儿子节点,比如区间加,那么我们下传 x x x标记的时候要直接改变 x x x左右儿子的key值,否则如果他的某个儿子没有访问到,然后update一下就出锅了,当然也有标记直接修改 x x x的子树就行了,不用去看儿子的子树,比如翻转标记。
如果要搞区间 [ l , r ] [l,r] [l,r],首先Splay维护的是位置,同时将 l − 1 l-1 l−1旋上根节点,然后将 r r r旋到 l − 1 l-1 l−1的右儿子,那么根节点右儿子以及右儿子的左子树就是这个区间了,然后打个标记。
具体实现看你们的了,要学的话去看看其他博主的实现,在这只提供思路,就不一一赘述。
小结
可以发现,复杂度都是与层数有关的,有人证明过,Splay的复杂度为 O ( m l o g n ) O(mlogn) O(mlogn),但是常数。。。
FHQ Treap
一听,treap?不就是随机数的那位?没错,Treap是在维护 k e y key key为平衡树的情况下,维护随机权值(建点时随机赋予的)呈小根堆的形式,普通的Treap也是旋转实现,但是我不会QAQ,常数也挺优秀,但是Treap不能区间操作,因为不够灵活,相反,FHQ Treap就是非旋Treap,利用分裂合并来进行神奇操作,而且分裂合并的灵活性让他可以支持区间操作,也就可以维护LCT(比Splay多个 l o g log log,其实只要能维护区间的树貌似都能维护LCT吧QMQ),因为不用旋转,所以共用某个节点也是可以的,所以又支持可持久化,唯一的缺点就是常数比Splay还大QAQ,但是区间操作貌似都海星。
例题
题目描述:
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
插入x数
删除x数(若有多个相同的数,因只删除一个)
查询x数的排名(排名定义为比当前数小的数的个数+1。若有多个相同的数,因输出最小的排名)
查询排名为x的数
求x的前驱(前驱定义为小于x,且最大的数)
求x的后继(后继定义为大于x,且最小的数)
输入格式:
第一行为n,表示操作的个数,下面n行每行有两个数opt和x,opt表示操作的序号(1≤opt≤6)
输出格式
对于操作3,4,5,6每行输出一个数,表示对应答案
输入输出样例:
输入:
10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598
输出:
106465
84185
492737
说明/提示:
时空限制:1000ms,128M
1.n的数据范围:n≤100000
2.每个数的数据范围:[-10^7,10^7]
来源:Tyvj1728 原名:普通