●小集训之旅 二
有志者自有千计万计,无志者只感千难万难。
●2017.3.28-29
●学习内容:伸展树 Splay Tree
引:二叉查找树(Binary Search Tree) 可以被用来表示有序集合、建立索引或优先队列等。最坏情况下,作用于二叉查找树上的基本操作的时间复杂度,可能达到O(n)。
●伸展树(Splay Tree)是二叉查找树的改进。
优点:对伸展树的操作的平摊复杂度是O(log2n)。伸展树的空间要求较低。
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的操作,为了使整个操作时间更小,被操作频率高的那些节点(子树)就应当经常处于靠近树根的位置。于是在每次操作之后对树进行重构,把被操作的节点(子树)搬移到离树根近一些的地方(并保证不破坏二叉树中各节点之间的关系)。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
(我们的Y老师曰:这是一种“玄学”算法,学了以后我感觉很有道理。)
(我们的Y老师还曰:Splay Tree是“区间王”,学了以后我也感觉很有道理。)
●算法相关内容(基础与支持操作)
- 单旋与双旋 rotate()(通过旋转操作使得x成为root)
- 单旋:在操作完位于节点x之后,对x进行旋转操作,使得x的父亲节点成为x的儿子节点 下面是两种情况:(x的父亲节点y是root,即fa[x]==y==root)
-
- 所以:
-
- 双旋:当x的父节点y的父节点z是根时,即fa[ fa[x] ]==z==root,则为了将x变为root,要进行两次旋转,那么便要分为两种情况来操作:(初学者当结论记吧,Y老师说是“玄学”)
- 同侧情况(即 x和y都为其父亲的左儿子或右儿子)
则先旋转y,再旋转x:(右旋—右旋 or 左旋—左旋)
- (直接由上面的两次单旋构成,每次关系变化的边只有两条,只是每次对象不同)
- 异侧情况(即x和y分别为其父亲的左儿子和右儿子)
则对x进行两次旋转:(左旋—右旋 or 右旋—左旋)
void rotate(int x,int &k) //旋转(单) { int y=fa[x],z=fa[y]; int l=(x!=c[y][0]),r=l^1; if(y==k) k=x; else c[z][y!=c[z][0]]=x; fa[x]=z; fa[y]=x; fa[c[x][r]]=y; c[y][l]=c[x][r]; c[x][r]=y; update(y); update(x); } void splay(int x,int &k) //伸展运动 ,e { int y,z; while(x!=k) { y=fa[x],z=fa[y]; if(y!=k) { if(c[y][0]==x^c[z][0]==y) rotate(x,k); else rotate(y,k); } rotate(x,k); } }
int find(int k,int x)//找树中的目标点 { pushdown(k); if(siz[c[k][0]]+1==x) return k; if(siz[c[k][0]]>=x) return find(c[k][0],x); else return find(c[k][1],x-siz[c[k][0]]-1); } int split(int k,int len)//裂(把需要的区间弄到根的右儿子的左儿子所在的子树上c[c[rt][1]][0]) { int x=find(rt,k),y=find(rt,k+len+1); splay(x,rt);splay(y,c[x][1]); return c[y][0]; }
#include<queue> #include<cmath> #include<cstdio> #include<cstring> #include<cstdlib> #include<iostream> #include<algorithm> #define inf 1000000000 #define N 1000005 using namespace std; int read()//读入优化 { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } int n,m,rt,cnt,k,len,val; char ch[10]; int a[N],id[N],fa[N],c[N][2]; int sum[N],siz[N],v[N],mx[N],lx[N],rx[N]; bool tag[N],rev[N]; queue<int> q; void update(int x) { int l=c[x][0],r=c[x][1]; sum[x]=sum[l]+sum[r]+v[x]; siz[x]=siz[l]+siz[r]+1; mx[x]=max(mx[l],mx[r]); mx[x]=max(mx[x],rx[l]+v[x]+lx[r]); lx[x]=max(lx[l],sum[l]+v[x]+lx[r]); rx[x]=max(rx[r],sum[r]+v[x]+rx[l]); } void pushdown(int x) { int l=c[x][0],r=c[x][1]; if(tag[x]) { rev[x]=tag[x]=0; //有tag 就不 rev if(l) tag[l]=1,v[l]=v[x],sum[l]=v[l]*siz[l]; if(r) tag[r]=1,v[r]=v[x],sum[r]=v[r]*siz[r]; if(v[x]>=0) { if(l) lx[l]=rx[l]=mx[l]=sum[l]; if(r) lx[r]=rx[r]=mx[r]=sum[r]; } else { if(l)lx[l]=rx[l]=0,mx[l]=v[x]; if(r)lx[r]=rx[r]=0,mx[r]=v[x]; } } if(rev[x]) { rev[x]^=1;rev[l]^=1;rev[r]^=1; //(^ 的妙处 ) swap(lx[l],rx[l]);swap(lx[r],rx[r]); swap(c[l][0],c[l][1]);swap(c[r][0],c[r][1]); } } void rotate(int x,int &k)//旋转(单) { int y=fa[x],z=fa[y]; int l=(x!=c[y][0]),r=l^1; if(y==k) k=x; else c[z][y!=c[z][0]]=x; fa[x]=z; fa[y]=x; fa[c[x][r]]=y; c[y][l]=c[x][r]; c[x][r]=y; update(y); update(x); } void splay(int x,int &k)//伸展运动 ,e { int y,z; while(x!=k) { y=fa[x],z=fa[y]; if(y!=k) { if(c[y][0]==x^c[z][0]==y) rotate(x,k); else rotate(y,k); } rotate(x,k); } } void build(int l,int r,int f)//建树 { if(l>r) return; int mid=l+r>>1,now=id[mid],last=id[f]; if(l==r) { sum[now]=a[l]; siz[now]=1; tag[now]=rev[now]=0; if(a[l]>=0)lx[now]=rx[now]=mx[now]=a[l]; else lx[now]=rx[now]=0,mx[now]=a[l]; } build(l,mid-1,mid);build(mid+1,r,mid); v[now]=a[mid]; fa[now]=last; c[last][mid>=f]=now; update(now); } int find(int k,int x)//找树中的目标点 { pushdown(k); if(siz[c[k][0]]+1==x) return k; if(siz[c[k][0]]>=x) return find(c[k][0],x); else return find(c[k][1],x-siz[c[k][0]]-1); } int split(int k,int len)//裂(把需要的区间弄到根的右儿子的左儿子所在的子树上c[c[rt][1]][0]) { int x=find(rt,k),y=find(rt,k+len+1); splay(x,rt);splay(y,c[x][1]); return c[y][0]; } void insert(int k,int len)//插入 { for(int i=1;i<=len;i++) if(!q.empty()) id[i]=q.front(),q.pop(); else id[i]=++cnt; for(int i=1;i<=len;i++) a[i]=read(); build(1,len,0); int x=id[1+len>>1]; int z=find(rt,k+1),y=find(rt,k+2);//第一位为-inf splay(z,rt); splay(y,c[z][1]); fa[x]=y; c[y][0]=x; update(y);update(fa[y]); } void rec(int x)//删除时“回收空间” (把不要的点的编号放进队列,下次要加新点时,直接用队列里的编号) { if(!x) return; int l=c[x][0],r=c[x][1]; rec(l); rec(r); q.push(x); fa[x]=c[x][0]=c[x][1]=0; tag[x]=rev[x]=0; } void erase(int k,int len)//删除区间 { int x=split(k,len),y=fa[x]; rec(x); c[y][0]=0; update(y);update(fa[y]); } void query(int k,int len)//询问区间和 { int x=split(k,len); printf("%d\n",sum[x]); } void rever(int k,int len)//区间翻转 { int x=split(k,len),y=fa[x]; if(!tag[x]) { rev[x]^=1; // ^ 的妙处 swap(lx[x],rx[x]); swap(c[x][0],c[x][1]); update(y);update(fa[y]); } } void modify(int k,int len,int val)//区间修改 { int x=split(k,len),y=fa[x]; tag[x]=1; v[x]=val; sum[x]=v[x]*siz[x]; if(v[x]>=0) lx[x]=rx[x]=mx[x]=sum[x]; else lx[x]=rx[x]=0,mx[x]=v[x]; update(y);update(fa[y]); } int main() { n=read();m=read(); mx[0]=a[1]=a[n+2]=-inf; id[1]=1; id[n+2]=n+2; for(int i=2;i<=n+1;i++) a[i]=read(),id[i]=i; build(1,n+2,0); rt=n+3>>1; cnt=n+2; while(m-->0) { scanf("%s",ch); if(ch[0]!='M'||ch[2]!='X') k=read(),len=read(); if(ch[0]=='I') insert(k,len); if(ch[0]=='D') erase(k,len); if(ch[0]=='R') rever(k,len); if(ch[0]=='G') query(k,len); if(ch[0]=='M') { if(ch[2]=='X')printf("%d\n",mx[rt]); else val=read(),modify(k,len,val); } } return 0; } /* ●需要 update() 的地方: build(), rotate(), modify(), rever(), erase(), insert(); ●需要 pushdown() 的地方: 程序中似乎只有 find() 中调用了pushdown(), 但众多操作中都通过 split()-->find()-->pushdown()来间接调用了pushdown() ○调用pushdown()的原则:在树的形态发生变化前要把lazy标记传下去; (split()中会调用splay(),使树的形态发生改变,所以split()中要先通过find()把lazy标记传下去) */
●总结:该算法的区间操作能力强大,时间空间也都比较优秀,无愧于“区间王”,但仍然有小小一点缺陷:1.常数过大,容易被卡。2.代码长,函数多,容易打错,所以要多多练习,把这些函数打熟练。
Do not go gentle into that good night.
Rage, rage against the dying of the light.
————Dylan Thomas