关于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大佬的帮助】

posted @ 2018-10-04 16:54  Ishtar~  阅读(352)  评论(0编辑  收藏  举报