学习笔记 【平衡树 Splay】

平衡树 Splay

\(\text{Splay}\)是平衡树的一种,中文名为伸展树。

它的主要思想是:对于查找频率较高的节点,使其处于离根节点相对较近的节点。这样就可以保证了查找的效率。

旋转操作:

为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。

旋转需要保证:

整棵 \(\text{Splay}\) 的中序遍历不变(不能破坏二叉查找树的性质)。
受影响的节点维护的信息依然正确有效。
\(\text{root}\) 必须指向旋转后的根节点。

\(\text{Splay}\) 中旋转分为两种:左旋和右旋。

具体分析旋转步骤(假设需要旋转的节点为 \(x\)

  1. \(y\)\(x\) 的父亲, \(z\)\(y\) 的父亲;
  2. \(z\) 的某一儿子为 \(y\) ,那么 \(z\) 的那个儿子变成 \(x\)\(x\) 的父亲就成了 \(z\)
  3. \(y\) 的某一儿子变成了 \(x\) 的另一儿子,相应的父亲改变;
  4. \(x\) 的另一儿子变成 \(y\) ,然后父亲改变。
inline void rotate(int x){
	int y=fa[x],z=fa[y];
	int k=son[y][1]==x;
	son[z][son[z][1]==y]=x,fa[x]=z;
	son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;
	son[x][k^1]=y,fa[y]=x;
	pushup(y),pushup(x);//更新信息
}

Splay操作:

\(\text{Splay}\) 规定:每访问一个节点后都要强制将其旋转到根节点。

  1. 如果 \(x\) 的父亲是目标节点,直接将其旋转;
  2. 如果 \(x\) 的父亲不是目标节点,且 \(y\) 的儿子类型和 \(x\) 的儿子类型相同,先转 \(y\) ,再转 \(x\)
  3. 上述,如果儿子类型不同,先转两次 \(x\)
inline void splay(int x,int goal){
	while(fa[x]!=goal){
		int y=fa[x],z=fa[y];
		if(z!=goal)
			((son[y][1]==x)^(son[z][1]==y))?rotate(x):rotate(y);//类型相同?转x:转y
		rotate(x);//再转x
	}
	if(!goal)root=x;//如果目标位置是根,那么根为x
}

插入操作:

沿用二叉查找树的方法,找到新节点应处的位置,新建节点后,再转到根。

inline void insert(int v){
	int u=root,f=0;
	while(u&&val[u]!=v)
		f=u,u=son[u][v>val[u]];//找x位置
	if(u) cnt[u]++;//若已有和x值一样的节点,那么个数++
	else{
		u=++tot;
		if(f) son[f][v>val[f]]=u;//某一儿子
		val[u]=v,fa[u]=f;//新建节点
		siz[u]=cnt[u]=1;
	}
	splay(u,0);//转到根
}

找前驱后继:

首先我们通过 \(\text{find}\) 函数将要查的节点转到根。

inline void find(int v){
	int u=root;
	if(!u)return;
	while(son[u][v>val[u]]&&v!=val[u])
		u=son[u][v>val[u]];
	splay(u,0);//转到根
}

看代码吧[捂脸]

inline int Nxt(int v,bool f){//f=0找前驱
	find(v);
	int u=root;
	if(val[u]>v&&f)return u;//若val比v大且要找后继,直接返回u
	if(val[u]<v&&!f)return u;//反之
	u=son[u][f];
	while(son[u][f^1]) u=son[u][f^1];//否则向某一方向子树的另一方向走到头。
	return u;
}

查询一个数的排名:

分三种情况讨论:

  1. 找到了这个数,那么排名为左子树的大小;
  2. 当前节点值比 \(x\) 大那么递归进左子树;
  3. 当前节点值比 \(x\) 小那么递归进右子树,排名为他在右子树的排名加上左子树大小(包括重复)。
inline int arank(int u,int v){
	if(!u)return 0;
	if(v==val[u])return siz[son[u][0]];//case 1
	if(v<val[u]) return arank(son[u][0],v);//case 2
	return arank(son[u][1],v)+siz[son[u][0]]+cnt[u];//case 3
}

查询排名为x的数

也是分三种情况讨论:

  1. 当前节点左子树大小大于 \(rank\) ,递归进左子树;
  2. 当前节点左子树大小加上重复的个数大于 \(rank\) ,返回当前值;
  3. 否则递归进右子树,在右子树中,\(x\) 的排名为 \(rank\) -左儿子大小-重复个数。
inline int aval(int u,int rank){
	if(siz[son[u][0]]>=rank) //case 1
		return aval(son[u][0],rank);
	if(siz[son[u][0]]+cnt[u]>=rank)//case 2
		return val[u];
	return aval(son[u][1],rank-siz[son[u][0]]-cnt[u]); //case 3
}

删除一个点

流程为:

  1. 找到这个点的前驱,后继;
  2. 将前驱转到根,将后继转到前驱下面;
  3. 那么此时后继的左儿子即为要删除的点。
inline void remove(int v){
	int pre=Nxt(v,0);
	int nxt=Nxt(v,1);//找前驱后继
	splay(pre,0);splay(nxt,pre);//转
	int del=son[nxt][0];
	if(cnt[del]>1){//若有重复,减即可
		cnt[del]--;
		splay(del,0);还需转到根
	}
	else son[nxt][0]=0;//儿子清0
}

\(\text{Splay}\)的基本操作就完成了,后续添加相关题目。

例题:

P2234 [HNOI2002]营业额统计

P1486 [NOI2004] 郁闷的出纳员

P2042 [NOI2005] 维护数列

posted @ 2021-07-29 19:51  Mr_think  阅读(79)  评论(0)    收藏  举报