●小集训之旅 二
有志者自有千计万计,无志者只感千难万难。
●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




浙公网安备 33010602011771号