splay:优雅的区间暴力!

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

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

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

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

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

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

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

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

如图即为一颗二叉搜索树

这棵树最基本的性质就是对于任何一个节点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重要的操作:旋转!旋转如图:

下面贴上右旋的伪代码(左旋同理,只不过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 其实是蒟蒻偷懒)

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

然后我们做一个总结:

对于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][2],tn,fa[MN],val[MN],sum[MN],cnt;
void update(int x){sz[x]=sz[c[x][0]]+sz[c[x][1]]+sum[x];}
void rotate(int x){
    int f=fa[x],ff=fa[f],l=c[f][1]==x,r=l^1;
    fa[f]=x,fa[x]=ff,fa[c[x][r]]=f;
    c[ff][c[ff][1]==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]][1]==f^c[f][1]==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]=1;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]>1){sum[x]--,splay(x,rtf);return;}int tmp;
        if(!(c[x][0]*c[x][1])){c[f][ty]=c[x][0]+c[x][1],fa[c[x][c[x][1]!=0]]=f;return;}
        for(tmp=c[x][1];c[tmp][0]!=0;tmp=c[tmp][0]);
        splay(x,rtf),splay(tmp,rt);
        c[tmp][0]=c[x][0],rt=tmp,fa[c[x][0]]=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][0]])return findk(c[x][0],k);
    else if(k>sz[c[x][0]]&&k<=sz[c[x][0]]+sum[x]){splay(x,rtf);return val[x];}
    else return findk(c[x][1],k-=sz[c[x][0]]+sum[x]);
}
int getk(int x,int k){
    if(!x)return 0;
    if(val[x]>=k)return getk(c[x][0],k);
    else return sz[c[x][0]]+sum[x]+getk(c[x][1],k);
}
int pre(int x){int tmp=getk(rt,x);return findk(rt,tmp);}
int suf(int x){int tmp=getk(rt,x+1)+1;return findk(rt,tmp);}
int main(){
    scanf("%d",&n);int opt,x;
    for(int i=1;i<=n;i++){
        scanf("%d%d",&opt,&x);
        if(opt==1)ins(rtf,0,x);
        else if(opt==2)del(rtf,0,x);
        else if(opt==3)printf("%d\n",getk(rt,x)+1);
        else if(opt==4)printf("%d\n",findk(rt,x));
        else if(opt==5)printf("%d\n",pre(x));
        else if(opt==6)printf("%d\n",suf(x));
    }
}

原文地址:https://www.cnblogs.com/ghostfly233/p/8261193.html

时间: 2024-11-02 02:34:25

splay:优雅的区间暴力!的相关文章

Splay树(区间更新)—— POJ 3468 A Simple Problem with Integers

对应POJ 题目:点击打开链接 A Simple Problem with Integers Time Limit: 5000MS   Memory Limit: 131072K Total Submissions: 72765   Accepted: 22465 Case Time Limit: 2000MS Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operati

Splay树(区间第k小)——POJ 2761 Feed the dogs

对应POJ题目:点击打开链接 Feed the dogs Time Limit: 6000MS   Memory Limit: 65536K Total Submissions: 16655   Accepted: 5203 Description Wind loves pretty dogs very much, and she has n pet dogs. So Jiajia has to feed the dogs every day for Wind. Jiajia loves Win

【bzoj1552/3506】[Cerc2007]robotic sort splay翻转,区间最值

[bzoj1552/3506][Cerc2007]robotic sort Description Input 输入共两行,第一行为一个整数N,N表示物品的个数,1<=N<=100000.第二行为N个用空格隔开的正整数,表示N个物品最初排列的编号. Output 输出共一行,N个用空格隔开的正整数P1,P2,P3…Pn,(1 < = Pi < = N),Pi表示第i次操作前第i小的物品所在的位置. 注意:如果第i次操作前,第i小的物品己经在正确的位置Pi上,我们将区间[Pi,Pi]

HihoCoder1677 : 翻转字符串(Splay)(区间翻转)

描述 给定一个字符串S,小Hi希望对S进行K次翻转操作. 每次翻转小Hi会指定两个整数Li和Ri,表示要将S[Li..Ri]进行翻转.(S下标从0开始,即S[0]是第一个字母) 例如对于S="abcdef",翻转S[2..3] 得到S="abdcef":再翻转S[0..5]得到S="fecdba". 输入 第一行包含一个由小写字母组成的字符串S. 第二行包含一个整数K. 以下K行每行包含两个整数Li和Ri. 对于50%的数据,1 ≤ |S| ≤

HYSBZ - 1269 文本编辑器editor (Splay 字符串的区间操作)

文本编辑器editor Time Limit: 10000MS   Memory Limit: 165888KB   64bit IO Format: %lld & %llu Description 这些日子,可可不和卡卡一起玩了,原来可可正废寝忘食的想做一个简单而高效的文本编辑器.你能帮助他吗?为了明确任务目标,可可对“文本编辑器”做了一个抽象的定义:   文本:由0个或多个字符构成的序列.这些字符的ASCII码在闭区间[32, 126]内,也就是说,这些字符均为可见字符或空格.光标:在一段文

文艺平衡树 lg3391(splay维护区间入门)

splay是支持区间操作的,先做这道题入个门 大多数操作都和普通splay一样,就不多解释了,只解释一下不大一样的操作 #include<bits/stdc++.h> using namespace std; #define INF 0x3f3f3f3f inline int read(){ int w=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') f=-1; ch=getchar(); } while(c

洛谷 P3391 【模板】文艺平衡树(Splay)

题目背景 这是一道经典的Splay模板题--文艺平衡树. 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1 输入格式: 第一行为n,m n表示初始序列有n个数,这个序列依次是 (1,2,?n?1,n) m表示翻转操作次数 接下来m行每行两个数 [l,r][l,r] 数据保证 1≤l≤r≤n 输出格式: 输出一行n个数字,表示原始序列经过m次变换后的结果

[LuoguP2161[ [SHOI2009]会场预约 (splay)

题面 传送门:https://www.luogu.org/problemnew/show/P2161 Solution splay 的确有线段树/树状数组的做法,但我做的时候脑残没想到 我们可以考虑写一个类似NOIP2017D2T3列队那道题那样的带分裂的平衡树 考虑用splay维护每一条线段的左端点和右端点 因为我们题目的意思保证了在平衡树里的线段不相交,所以我们可以考虑以下的性质 每一条线段作为一个点放入平衡树中,维护其L,R,并记录它是空白线段还是有预约的线段 我们要查询一段区间,设这个区

【BZOJ3786】星系探索 DFS序+Splay

[BZOJ3786]星系探索 Description 物理学家小C的研究正遇到某个瓶颈. 他正在研究的是一个星系,这个星系中有n个星球,其中有一个主星球(方便起见我们默认其为1号星球),其余的所有星球均有且仅有一个依赖星球.主星球没有依赖星球. 我们定义依赖关系如下:若星球a的依赖星球是b,则有星球a依赖星球b.此外,依赖关系具有传递性,即若星球a依赖星球b,星球b依赖星球c,则有星球a依赖星球c. 对于这个神秘的星系中,小C初步探究了它的性质,发现星球之间的依赖关系是无环的.并且从星球a出发只