文章目录
有关splay的一些事
1.关于定义
splay原名伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特(Daniel Sleator) 和 罗伯特·恩卓·塔扬(Robert Endre Tarjan) 在1985年发明的。
伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
2.实现部分
0.前置知识
新建节点:
int get(int &x,int v,int fa)
{
x=++tot;
key[x]=v;
f[x]=fa;
size[x]=1;
}
让序列中的第x个数旋转到根的:
int st(int y,int z)
{
int x=root;
while (size[L]+1!=y)
if (y<size[L]+1)
x=L;
else
y-=size[L]+1,x=R;
splay(x,z);
return x;
}
pd(x)表示x是其父亲的哪个儿子(0为左,1为右)。
1.旋转(rotate)
首先,最重要的就是旋转部分了。比较重要的就是理解其核心思想。
其中,旋转分为单旋和双旋,图一到图二是单旋的基本结构。
图一
图二
用语言描述即为:
- 将y的左儿子设为x的右儿子。 ( s o n [ y ] [ 0 ] = s o n [ x ] [ 1 ] ) (son[y][0]=son[x][1]) (son[y][0]=son[x][1])
- 把原x的右儿子的父亲设为y。 ( f [ s o n [ x ] [ 1 ] ] = y ) (f[son[x][1]]=y) (f[son[x][1]]=y)
- 将y的父亲的左或右(具体看y是它的左还是右儿子)儿子变为x。 ( s o n [ f [ y ] ] [ p d ( y ) ] = x ) (son[f[y]][pd(y)]=x) (son[f[y]][pd(y)]=x)
- 将x的父亲变为原来y的父亲。 ( f [ x ] = f [ y ] ) (f[x]=f[y]) (f[x]=f[y])
- 把x的右儿子变成y。 ( s o n [ x ] [ 1 ] = y ) (son[x][1]=y) (son[x][1]=y)
- 将y的父亲设为x。 ( f [ y ] = x ) (f[y]=x) (f[y]=x)
- 最后记得update。
int rotate(int x)
{
int y=f[x],k=pd(x);
son[y][k^1]=son[x][k];
f[son[x][k]]=y;
son[f[y]][pd(y)]=x;
f[x]=f[y];
son[x][k]=y;
f[y]=x;
update(y),update(x);
}
2.伸展(splay)
而在splay里面,就需要用到双旋(即对于爷爷,父亲和儿子三点共线时,先旋父亲,再旋儿子)。
如图。
图三
先rotate(y),变为:
图四
再rotate(x),变为:
图五
嗯,是这样吧…
接着就完成了一次双旋。
然而,对于splay(x,y),我们需要做的是将x旋转到y的儿子的位置。
所以我们要不断地旋转它(x),直到它成为y的儿子。
int splay(int x,int g)
{
while (f[x]!=g)
{
int y=f[x],z=f[y];
if (z!=g)
if (pd(x)==pd(y))
rotate(y);//此处为双旋(先旋父亲)
else
rotate(x);//单旋
rotate(x);
}
if (!g)
root=x;
}
3.建spaly tree(build)
其实建树有很多方法,这里讲比较通用的一种。
就是像线段树一样从root节点开始,每次分到它的左右儿子去。
int build(int &x,int l,int r,int fa)
{
mid=(l+r)/2;
if (l<=r)
get(x,a[mid],fa),build(son[x][0],l,mid-1,x),build(son[x][1],mid+1,r,x),update(x);
}
当然,还有比较暴力的方法:
1.先建成一条链,再通过splay使其尽量平衡。
2.建一个只有根节点的树,再不断插入(insert)节点。
具体视情况而定。
4.插入(insert)
方法一:插入值为x的点,那么我们可以先找到它的前驱以及后继,再新建节点,并且将该节点设为前驱和后继的父亲。
方法二:将这个点的前驱设为根,后继设为根的右儿子,那么直接在后继的左儿子处增加这个节点即可。
很好理解吧。
以下代码为方法二:
int ins(int x,int y)
{
st(x,0);
st(x+1,root);
get(son[son[root][1]][0],y,son[root][1]);
update(son[root][1]),update(root);
}
5.删除(del)
由于delete为系统自带函数,所以改下名。
跟插入的道理其实差不多,将x的前驱旋到更,后继后继设为根的右儿子,那么后继的左儿子只能为x(证明显然)。
删掉就好了。
int del(int x)
{
st(x,0);
st(x+2,root);
son[son[root][1]][0]=0;
update(son[root][1]),update(root);
}
6.替换(replace)
其实道理很简单,就是在splay tree里找到节点所在的位置,然后直接修改更新就好了。
int replace(int a,int b)
{
int x=root;
a++;
while (size[L]+1!=a)
if (a<size[L]+1)
x=L;
else
a-=size[L]+1,x=R;
key[x]=b,splay(x,0);
}
7.统计答案(getans)
这个视不同情况而定,很难写,但是总的来说就是:
比如统计x~y的答案,那么我们就直接将x旋到根,y的后继旋到根的左儿子,同理,y的后继的左子树即为答案。
3.一些废话
- 其实splay之前学过,但是一直搞不懂它的内涵,一直靠背代码为生。
- 但是经过本次的(
非深入)学习,大概明白了splay的内涵,期待可以在考场上熟练的码出正确的splay(像线段树那样) - 希望通过本次的学习,对splay不再畏惧。
4.不知道是不是废话的话
- splay要尽量打在struct里,不要问为什么,常数优化。
- 对于一些很难维护的东西,要考虑两点:
- 是不是可以通过套用其他的算法或者结构解决。
- 实在想不到的话,可以先转化成线段树式的维护,再通过类比思想转移过来。
- 在维护时,记得该update的地方一定要update(否则正确率担忧),不该update的地方一定不要update(否则时间担忧),所以在打splay时一定要考虑清楚了再打。
- 使用#define可以节省很多代码量,也可以节省很多时间。
- 切记!!!空间一定要开够,但是不要爆掉!!!
- 必要时可以建一个栈来存放无用节点,可以节省很多空间。
- 其实splay也叫spaly…
(中考加油!)
预告:下一篇平衡树学习笔记(2)——替罪羊树