平衡树学习笔记(1)——splay

有关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)

首先,最重要的就是旋转部分了。比较重要的就是理解其核心思想。

其中,旋转分为单旋和双旋,图一到图二是单旋的基本结构。

在这里插入图片描述

图一

在这里插入图片描述

图二

用语言描述即为:

  1. 将y的左儿子设为x的右儿子。 ( s o n [ y ] [ 0 ] = s o n [ x ] [ 1 ] ) (son[y][0]=son[x][1]) (son[y][0]=son[x][1])
  2. 把原x的右儿子的父亲设为y。 ( f [ s o n [ x ] [ 1 ] ] = y ) (f[son[x][1]]=y) (f[son[x][1]]=y)
  3. 将y的父亲的左或右(具体看y是它的左还是右儿子)儿子变为x。 ( s o n [ f [ y ] ] [ p d ( y ) ] = x ) (son[f[y]][pd(y)]=x) (son[f[y]][pd(y)]=x)
  4. 将x的父亲变为原来y的父亲。 ( f [ x ] = f [ y ] ) (f[x]=f[y]) (f[x]=f[y])
  5. 把x的右儿子变成y。 ( s o n [ x ] [ 1 ] = y ) (son[x][1]=y) (son[x][1]=y)
  6. 将y的父亲设为x。 ( f [ y ] = x ) (f[y]=x) (f[y]=x)
  7. 最后记得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.不知道是不是废话的话

  1. splay要尽量打在struct里,不要问为什么,常数优化。
  2. 对于一些很难维护的东西,要考虑两点:
    1. 是不是可以通过套用其他的算法或者结构解决。
    2. 实在想不到的话,可以先转化成线段树式的维护,再通过类比思想转移过来。
  3. 在维护时,记得该update的地方一定要update(否则正确率担忧),不该update的地方一定不要update(否则时间担忧),所以在打splay时一定要考虑清楚了再打。
  4. 使用#define可以节省很多代码量,也可以节省很多时间。
  5. 切记!!!空间一定要开够,但是不要爆掉!!!
  6. 必要时可以建一个栈来存放无用节点,可以节省很多空间。
  7. 其实splay也叫spaly…

(中考加油!)

预告:下一篇平衡树学习笔记(2)——替罪羊树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值