关于Splay的学习感受
【关于Splay】
之前记得五月份听过一次外省金牌选手讲过一次,然后七月份又讲过一次,但本人脑子比较笨,当时完全听得一脸懵逼啊,练了两个月确实不一样,现在谈一下学习Splay的一些感受。
首先欲知Splay为何物,不得不先讲讲它的祖宗:二叉查找树,即BST(Binary Search Tree),关于二叉查找树,它不是一颗空树就是拥有以下几个性质的二叉树:
1.树中每个结点被赋予了一个权值;(下面假设不同结点的权值互不相同。)
2.若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
3.若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
4.左、右子树也分别为二叉查找树;
下图就是一个合法的BST(蜜汁画风【捂脸】):
通常二叉查找树支持如下操作:查找一个元素x在集合中是否存在;插入或删除一个元素,这些都可以通过递归实现。
但这也有个小问题,每个操作的复杂度都取决于树的高度,那么当这棵树退化成一条链的时候,复杂度就退化成O(n)的了,由于二叉查找树的形态不一,所以平衡树就出现了,它的每种操作最坏情况、均摊、期望的时间复杂度就是O(log n)的。而常用平衡树有两种:Splay和Treap,这里介绍Splay,下一篇文章介绍Treap。
(废话一堆然后转入正题)
Splay,即伸展树,是二叉查找树的一种改进,它与二叉查找树性质相似,与其不同的就是它可以自我调整。
关于Splay操作:伸展操作 Splay(x, S)是在保持伸展树有序性的前提下,通过一系列旋转将伸展树 S 中的元素 x调整至树的根部。在调整的过程中,要分以下三种情况分别处理:这个东西常数不能靠谱的可持久化,所以比起Treap而言,Splay的优势似乎仅在于求LCT。
1.节点x的父节点y是根节点:如果x是左儿子,那么就右旋一下,如果是右儿子,那么就左旋一下,一次操作后,x就变成根节点了,完成,下面是示意图:
2.节点x的父节点y不是根节点,y的父节点为z,且x,y均为父节点的左儿子或右儿子,那么就右旋或左旋两次,分别交换y,z和x,y,下面是示意图(灵魂画师再次上线):
3.节点x的父节点y不是根节点,y的父节点为z,x,y一个是左儿子,一个是右儿子,这时如果x是左儿子就先右旋再左旋,如果是右儿子就先左旋再右旋,注意两次都是转x,示意图:
所以由上图可以感性理解一下,经旋转的平衡树确实比之前要平衡很多,注意每次操作后都要进行伸展操作,然后这样下来各操作复杂度均摊为O(log n)(因为不会证所以我就直接跳了)。
Splay支持以下操作:
1.Find(x,S):判断元素x是否在伸展树S表示的有序集中。首先,访问根节点,如果x比根节点权值小则访问左儿子;如果x比根节点权值大则访问右儿子;如果权值相等,则说明x在树中;如果访问到空节点,则x不在树中。如果x在树中,则再执行Splay(x,S)调整伸展树。
2.Insert(x,S):将元素x插入伸展树S表示的有序集中。首先,访问根节点,如果x比根节点权值小则访问左儿子;如果x比根节点权值大则访问右儿子;如果访问到空节点t,则把x插入该节点,然后执行Splay(t,S)。
3.Merge(S1,S2):将两个伸展树S1与S2合并成为一个伸展树。其中S1的所有元素都小于S2的所有元素。首先,我们找到伸展树S1中最大的一个元素x,再通过Splay(x,S1)将x调整到伸展树S1的根。然后再将S2作为x节点的右子树。这样,就得到了新的伸展树S。如图所示:
4.Delete(x,S):把节点x从伸展树表示的有序集中删除。首先,执行Splay(x,S)将x旋转至根节点。然后取出x的左子树S1和右子树S2,执行Merge(S1,S2)把两棵子树合并成S。如图所示:
5.Split(x,S):以x为界,将伸展树S分离为两棵伸展树S1和S2,其中S1中所有元素都小于x,S2中的所有元素都大于x。首先执行Find(x,S),将元素x调整为伸展树的根节点,则x的左子树就是S1,而右子树为S2。图示同上。
除了以上操作,Splay还支持求最大值、最小值、前驱后继,同时,Splay还能进行区间操作,对于待操作区间[l,r],我们将l-1通过Splay旋至根节点处,再将r+1旋至根节点右儿子处,这时r+1的左儿子就是待操作区间,可以像线段树那样打lazy-tag标记,然后具体实现见下方代码(主要给Rotate(左旋右旋合并版,只用打一段即可,不必打一个Zig打一个Zag),Splay,build,lazy,find(int k)(寻找第k大数),getnext(求后继,前驱类似)):
(因为从来不打指针所以只能够打打数组)
结构体:
struct Num{ //根据题目不同决定
int val,id;
bool operator<(const Num &a)const{
if(val==a.val)
return id<a.id;
return val<a.val;
}
}So[MAXN/5];
struct Tr{ //平衡树
int fa,sum;
int val,c[2],lz;
}tr[MAXN];
int tot,root,n;
1.Rotate:
void Rotate(int x,int k)
{
if(tr[x].fa==-1)
return ;
int fa=tr[x].fa,w;
lazy(fa);
lazy(x);
tr[fa].c[!k]=tr[x].c[k];
if(tr[x].c[k]!=-1)
tr[tr[x].c[k]].fa=fa;
tr[x].fa=tr[fa].fa,tr[x].c[k]=fa;
if(tr[fa].fa!=-1)
{
w=tr[tr[fa].fa].c[1]==fa;
tr[tr[fa].fa].c[w]=x;
}
tr[fa].fa=x;
Push(fa);
Push(x);
}
2.Splay
void Splay(int x,int goal)
{
if(x==-1)
return ;
lazy(x);
while(tr[x].fa!=goal)
{
int y=tr[x].fa;
lazy(tr[y].fa);
lazy(y),lazy(x);
bool w=x==tr[y].c[1];
if(tr[y].fa!=goal&&w==(y==tr[tr[y].fa].c[1]))
Rotate(y,!w);
Rotate(x,!w);
}
if(goal==-1)
root=x;
Push(x);
}
3.build
int build(int l,int r,int f) //返回根节点
{
if(r<l)
return -1;
int mid=l+r>>1;
int ro=newtr(mid,f,mid);
data[mid]=ro;
tr[ro].c[0]=build(l,mid-1,ro);
tr[ro].c[1]=build(mid+1,r,ro);
Push(ro);
return ro;
}
4.lazy
void lazy(int id) //此懒标记表示交换左右儿子,即区间反转
{
if(tr[id].lz)
{
swap(lc,rc);
tr[lc].lz^=1,tr[rc].lz^=1;
tr[id].lz=0;
}
}
5.find
int find(int k)
{
int id=root;
while(id!=-1)
{
lazy(id);
int lsum=(lc==-1)?0:tr[lc].sum;
if(lsum>=k)
{
id=lc;
}
else
if(lsum+1==k)
break;
else
{
k=k-lsum-1;
id=rc;
}
}
return id;
}
6.getnext
int Getnext(int id)
{
lazy(id);
int p=tr[id].c[1];
if(p==-1)
return id;
lazy(p);
while(tr[p].c[0]!=-1)
{
p=tr[p].c[0];
lazy(p);
}
return p;
}
关于Splay,在实现上还有许多细节,如每次操作后的Splay,此处不再赘述。
以上大概是我关于Splay的一些学习总结,以后可能还会填坑【PS:感谢wcr大佬的帮助】