这是一道经典的Splay模板题——文艺平衡树。html
您须要写一种数据结构(可参考题目标题),来维护一个有序数列,其中须要提供如下操做:翻转一个区间,例如原有序序列是5 4 3 2 1,翻转区间是[2,4]的话,结果是5 2 3 4 1c++
第一行为n,m n表示初始序列有n个数,这个序列依次是(1,2,⋯n−1,n) m表示翻转操做次数数组
接下来m行每行两个数 [l,r]数据保证 1≤l≤r≤n数据结构
输出格式:输出一行n个数字,表示原始序列通过m次变换后的结果函数
5 3
1 3
1 3
1 4
4 3 2 1 5
n,m≤100000 n, m性能
1 简介:
伸展树,或者叫自适应查找树,是一种用于保存有序集合的简单高效的数据结构。伸展树实质上是一个二叉查找树。容许查找,插入,删除,删除最小,删除最大,分割,合并等许多操做,这些操做的时间复杂度为O(logN)。因为伸展树能够适应需求序列,所以他们的性能在实际应用中更优秀。
伸展树支持全部的二叉树操做。伸展树不保证最坏状况下的时间复杂度为O(logN)。伸展树的时间复杂度边界是均摊的。尽管一个单独的操做可能很耗时,但对于一个任意的操做序列,时间复杂度能够保证为O(logN)。优化
2 自调整和均摊分析:
平衡查找树的一些限制:
一、平衡查找树每一个节点都须要保存额外的信息。
二、难于实现,所以插入和删除操做复杂度高,且是潜在的错误点。
三、对于简单的输入,性能并无什么提升。
平衡查找树能够考虑提升性能的地方:
一、平衡查找树在最差、平均和最坏状况下的时间复杂度在本质上是相同的。
二、对一个节点的访问,若是第二次访问的时间小于第一次访问,将是很是好的事情。
三、90-10法则。在实际状况中,90%的访问发生在10%的数据上。
四、处理好那90%的状况就很好了。ui
3 均摊时间边界:
在一颗二叉树中访问一个节点的时间复杂度是这个节点的深度。所以,咱们能够重构树的结构,使得被常常访问的节点朝树根的方向移动。尽管这会引入额外的操做,可是常常被访问的节点被移动到了靠近根的位置,所以,对于这部分节点,咱们能够很快的访问。根据上面的90-10法则,这样作能够提升性能。
为了达到上面的目的,咱们须要使用一种策略──旋转到根(rotate-to-root)。spa
上面所说的,我来举个例子解释一下:假设有这样一道题,有100000次操做,每次输入a,b,若a为0表示将b放入数列中,若a为1表示输出第b大的数。这道题看似简单,不就直接二叉搜索树嘛!可是若是数b是单调递增出现的,则树会成链,那么仍是O(n^2)的复杂度。此时咱们边用到了Splay,因为splay是不断翻转的,因此就算某一时刻他成了一条链,也会立刻旋转而变成另外的形态(深度减低),经过这样不断地变换能够防止长期停留在链的状态,以保证每次操做平均复杂度O(log n)。.net
关于Splay,我以为本身已经彻底掌握了,让我口头说还能够,可是要写篇详解实在是时间又少并且没精力(并且大神们的博客已经写的很是到位了,本身写的确定不及他们),因此这里我提供本人自学Splay时所看的一些比较有用的博客:一、基础(非指针) 二、基础(指针) 三、应用
认真看上述博客并思考,便会发现Splay其实很简单。
只要咱们弄懂Splay,其实本题很简单:首先按照中序遍历建树,而后对于每次修改区间l,r,首先得提出这段区间,方法是将l的前趋l-1旋转到根节点,将r的后趋r+1旋转到根节点的右儿子,咱们能够本身画图试试,容易发现通过这个操做后,根节点的右儿子的左子树(具体应该说是这个左子树的中序遍历)就是区间l-r。关键的翻转时,由于树是中序遍历(左根右),因此咱们只要将l-r(前面所说的根节点的右儿子的左子树)这个区间子树左右儿子的节点交换位置(这样再中序遍历至关于右根左,即作到了翻转操做)。关键是翻转的优化,咱们用到懒惰标记lazy[x](表示x是否翻转),每次翻转时只要某个节点有标记且在翻转的区间内,则将标记下放给它的两个儿子节点且将自身标记清0,这样便避免了多余的重复翻转。(不懂画图看博客)
一、裸代码:
// luogu-judger-enable-o2 #include<bits/stdc++.h> #define ll long long #define il inline #define debug printf("%d %s\n",__LINE__,__FUNCTION__) using namespace std; const int N=100005; il int gi() { int a=0;char x=getchar();bool f=0; while((x<'0'||x>'9')&&x!='-')x=getchar(); if(x=='-')x=getchar(),f=1; while(x>='0'&&x<='9')a=a*10+x-48,x=getchar(); return f?-a:a; } int n,m,tot,root,siz[N],fa[N],flag[N],key[N],ch[N][2],cnt[N],ans[N]; il void update(int rt) { int l=ch[rt][0],r=ch[rt][1]; siz[rt]=siz[l]+siz[r]+1; } il void pushdown(int now) { if(flag[now]){ flag[ch[now][0]]^=1; flag[ch[now][1]]^=1; swap(ch[now][0],ch[now][1]); flag[now]=0; } } il int getson(int x){return ch[fa[x]][1]==x;} il void rotate(int x) { int y=fa[x],z=fa[y],b=getson(x),c=getson(y),a=ch[x][!b]; if(z)ch[z][c]=x;else root=x;fa[x]=z; if(a)fa[a]=y;ch[y][b]=a; ch[x][!b]=y;fa[y]=x; update(y);update(x); } il void splay(int x,int i) { while(fa[x]!=i){ int y=fa[x],z=fa[y]; if(z==i)rotate(x); else { if(getson(x)==getson(y)){rotate(y);rotate(x);} else {rotate(x);rotate(x);} } } } il int find(int x) { int now=root; while(1){ pushdown(now); if(ch[now][0]&&x<=siz[ch[now][0]])now=ch[now][0]; else { int tmp=(ch[now][0]?siz[ch[now][0]]:0)+1; if(x<=tmp)return now; x-=tmp; now=ch[now][1]; } } } il int build(int l,int r,int rt) { int now=l+r>>1; fa[now]=rt; key[now]=ans[now]; if(l<now)ch[now][0]=build(l,now-1,now); if(r>now)ch[now][1]=build(now+1,r,now); update(now); return now; } il void print(int now) { pushdown(now); if(ch[now][0])print(ch[now][0]); ans[++tot]=key[now]; if(ch[now][1])print(ch[now][1]); } int main() { n=gi(),m=gi();int x,y; for(int i=1;i<=n+2;i++)ans[i]=i-1; root=build(1,n+2,0); for(int i=1;i<=m;i++){ x=gi(),y=gi(); x=find(x),y=find(y+2); splay(x,0);splay(y,x); flag[ch[ch[root][1]][0]]^=1; } print(root); for(int i=1;i<=n;i++)printf("%d ",ans[i+1]); return 0; }
二、方便理解,带注释代码:
/*Splay只记模板是很困难的,并且真正运用时易生疏出错,因此必须理解,在看代码前先弄懂 Splay的原理,这篇代码是带注释的Splay模板,题目来自洛谷P3391 ———————————by 520*/ #include<bits/stdc++.h> #define il inline #define debug printf("%d %s\n",__LINE__,__FUNCTION__) using namespace std; const int N=100005; il int gi() { int a=0;char x=getchar();bool f=0; while((x<'0'||x>'9')&&x!='-')x=getchar(); if(x=='-')x=getchar(),f=1; while(x>='0'&&x<='9')a=a*10+x-48,x=getchar(); return f?-a:a; } int n,m,tot,root,siz[N],fa[N],lazy[N],key[N],tree[N][2],ans[N]; /*root为根节点,siz存储子树节点数,fa存储父节点,lazy是懒惰标记用来标记区间翻转操做,key数组存储原数列,tree为 splay树,ans存储答案*/ il void pushup(int rt) //做用相似与线段树 { int l=tree[rt][0],r=tree[rt][1]; //pushup做用是将子树的节点个数更新给根节点 siz[rt]=siz[l]+siz[r]+1; } il void pushdown(int now) { if(lazy[now]){ lazy[tree[now][0]]^=1; lazy[tree[now][1]]^=1; /*pushdown做用是下放懒惰标记,若某一节点所在子树(即某一区间)被翻转 ,则将懒惰标记下放给两个儿子节点,交换左右儿子位置(中序遍历,交换左右儿子后至关于翻转)并对所在子树根节 点的标记清0,*/ swap(tree[now][0],tree[now][1]); lazy[now]=0; } } il int getson(int x){return tree[fa[x]][1]==x;} //getson判断x是其父亲的右儿子仍是左儿子 il void rotate(int x) //旋转操做,直接写在一个函数里,能够称为上旋 { int y=fa[x],z=fa[y],b=getson(x),c=getson(y),a=tree[x][!b]; /*y是x的父节点,z是y的父节点,getson解释过了。 特别解释一下a,a为旋转时须要移动的子树,若x为左儿子则右旋时要将x的右子树移动,同理若x为右儿子则左旋时要 将x的左子树移动,因此这里a=tree[x][!b]*/ if(z)tree[z][c]=x;else root=x;fa[x]=z; /*若z不为根节点,则用x替代y的位置;若z为根节点,则将x变为根节点。*/ if(a)fa[a]=y;tree[y][b]=a; /*若存在要移动的子树a,则把a和y相连,取代原来x的位置*/ tree[x][!b]=y;fa[y]=x; /*!b的缘由:若x为左儿子则旋转后y为x的右儿子,若x为右儿子则旋转后y为x的左儿子。记得将y 指向x*/ pushup(y);pushup(x); /*旋转后,对被移动了的y和x更新它们各自的子树节点数*/ } il void splay(int x,int i) { while(fa[x]!=i){ //只要x没有旋转到须要的点下面,则一直旋,注意根节点的父亲为虚点0 int y=fa[x],z=fa[y]; if(z==i)rotate(x); //若x的爷爷是i,则只需旋一次 else { if(getson(x)==getson(y)){rotate(y);rotate(x);} /*若x和y为相同偏向,则进行Zig-Zig或Zag-Zag操做*/ else {rotate(x);rotate(x);} /*不然进行Zig-Zag或Zag-Zig操做*/ /*注意rotate函数中已经包含了这四种操做状况了*/ } } } il int find(int x) //查找x的位置 { int now=root; //从根节点往下 while(1){ pushdown(now); //本次操做要将前面的标记进行翻转 if(tree[now][0]&&x<=siz[tree[now][0]])now=tree[now][0]; //若存在左子树且x小于等于左子树的节点数,则x在左子树上 else { int tmp=(tree[now][0]?siz[tree[now][0]]:0)+1; //往右子树找,+1表明加上这个子树的根节点 if(x==tmp)return now; //若找到了x,返回它的位置 x-=tmp; //不然x减去根节点右子树之外的节点数,这个画图能理解,由于siz值并非直接的x的值 now=tree[now][1]; //将原来根节点的右儿子赋为新的根节点,继续递归查找x位置 } } } il int build(int l,int r,int rt) //建树过程和线段树相似 { int now=l+r>>1; fa[now]=rt; key[now]=ans[now]; //key存原数组1到n,准确说是0到n+1,缘由是主函数里的预处理 if(l<now)tree[now][0]=build(l,now-1,now); if(r>now)tree[now][1]=build(now+1,r,now); pushup(now); //记得pushup return now; } il void print(int now) //输出时中序遍历,按左根右输出 { pushdown(now); //记得要翻转 if(tree[now][0])print(tree[now][0]); //由于中序遍历左根右,因此递归根节点左子树到第一个数的位置 ans[++tot]=key[now]; //回溯时存储答案,注意咱们翻转操做的是原数组下标 if(tree[now][1])print(tree[now][1]); //同理递归根节点的右子树 } int main() { n=gi(),m=gi();int x,y; for(int i=1;i<=n+2;i++)ans[i]=i-1; /*由于取出操做区间时旋转的是x的前驱和y的后驱,因此预处理时第i个点 存的是i的前驱*/ root=build(1,n+2,0); while(m--) { x=gi(),y=gi(); x=find(x),y=find(y+2); /*查找x的前驱所在的位置,和y后驱所在的位置,由于预处理时ans存的是前趋, 因此直接查找x,而y的后驱变成了y+2*/ splay(x,0);splay(y,x); /*将x前驱上旋至根节点,y的后驱上旋成根节点右儿子的左子树*/ lazy[tree[tree[root][1]][0]]^=1;//通过旋转后,此时根节点的右儿子的左子树就是须要翻转的区间,因此lazy标记 } print(root); for(int i=1;i<=n;i++)printf("%d ",ans[i+1]); //输出时将前驱还原为原数 return 0; }