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][],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));
}
}
splay:优雅的区间暴力!的更多相关文章
- 2018牛客网暑期ACM多校训练营(第三场) H - Shuffle Cards - [splay伸展树][区间移动][区间反转]
题目链接:https://www.nowcoder.com/acm/contest/141/C 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 262144K,其他语言524288K ...
- HihoCoder1677 : 翻转字符串(Splay)(区间翻转)
描述 给定一个字符串S,小Hi希望对S进行K次翻转操作. 每次翻转小Hi会指定两个整数Li和Ri,表示要将S[Li..Ri]进行翻转.(S下标从0开始,即S[0]是第一个字母) 例如对于S=" ...
- 【bzoj1552/3506】[Cerc2007]robotic sort splay翻转,区间最值
[bzoj1552/3506][Cerc2007]robotic sort Description Input 输入共两行,第一行为一个整数N,N表示物品的个数,1<=N<=100000. ...
- 伸展树splay之求区间极值
前言 这篇博客是根据我在打这道题的时候遇到的问题,来打的,有些细节可能考虑不到. 题目 在N(1<=N<=100000)个数A1-An组成的序列上进行M(1<=M<=10000 ...
- POJ3580 SuperMemo splay伸展树,区间操作
题意:实现一种数据结构,支持对一个数列的 6 种操作:第 x 个数到第 y 个数之间的数每个加 D:第 x 个数到第 y 个数之间全部数翻转:第 x 个数到第 y 个数之间的数,向后循环流动 c 次, ...
- POJ-3468 A Simple Problem with Integers Splay Tree区间练习
题目链接:http://poj.org/problem?id=3468 以前用线段树做过,现在用Splay Tree A了,向HH.kuangbin.cxlove大牛学习了各种Splay各种操作,,, ...
- bzoj3223 Tyvj 1729 文艺平衡树(Splay Tree+区间翻转)
3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 2202 Solved: 1226[Submit][Sta ...
- P2042 [NOI2005]维护数列 && Splay区间操作(四)
到这里 \(A\) 了这题, \(Splay\) 就能算入好门了吧. 今天是个特殊的日子, \(NOI\) 出成绩, 大佬 \(Cu\) 不敢相信这一切这么快, 一下子机房就只剩我和 \(zrs\) ...
- 文艺平衡树 lg3391(splay维护区间入门)
splay是支持区间操作的,先做这道题入个门 大多数操作都和普通splay一样,就不多解释了,只解释一下不大一样的操作 #include<bits/stdc++.h> using name ...
随机推荐
- 第三章 AOP
什么是AOP AOP的编写方式 什么是AOP? 是一种面向切面的思想,关注的是切面中的相似功能,将这些功能抽离出来,提高代码的复用性 AOP术语 advice-通知:要执行的任务 Spring切面有5 ...
- Python 源码剖析(六)【内存管理机制】
六.内存管理机制 1.内存管理架构 2.小块空间的内存池 3.循环引用的垃圾收集 4.python中的垃圾收集 1.内存管理架构 Python内存管理机制有两套实现,由编译符号PYMALLOC_DEB ...
- Dom事件的三种绑定方式
1.事件 2. onclick, onblur, onfocus, 需求:请写出一个行为,样式,结构,相分离的页面. JS, CSS, HTML, 示例1,行为结构样式粘到一起的页面: & ...
- P2805 [NOI2009]植物大战僵尸(最小割+拓扑排序)
题意: n*m的矩阵,每个位置都有一个植物.每个植物都有一个价值(可以为负),以及一些它可以攻击的位置.从每行的最右面开始放置僵尸,僵尸从右往左行动,当僵尸在植物攻击范围内时会立刻死亡.僵尸每到一个位 ...
- [Leetcode] Construct binary tree from preorder and inorder travesal 利用前序和中续遍历构造二叉树
Given preorder and inorder traversal of a tree, construct the binary tree. Note: You may assume tha ...
- c++ linux 判断string是中文的 or 英文的 字符串。
#include <iostream> #include <string.h> #include <stdio.h> #include <stdlib.h&g ...
- HDU 5640
King's Cake Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)Total ...
- HDU1298 字典树+dfs
T9 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submissi ...
- JS学习之数组
- css 常用苹方字体
// 苹方-简 常规体 font-family: PingFangSC-Regular, sans-serif; // 苹方-简 极细体 font-family: PingFangSC-Ultrali ...