浅谈伸展树

みなさん、こんにちは。今天我们来讲解伸展树。

0.原题

(来自LuoGu P3369 【模板】普通平衡树)

题目描述

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

插入 x 数
删除 x 数(若有多个相同的数,因只删除一个)
查询 x 数的排名(排名定义为比当前数小的数的个数 +1 )
查询排名为 x 的数
求 x 的前驱(前驱定义为小于 x,且最大的数)
求 x 的后继(后继定义为大于 x,且最小的数)

输入格式

第一行为 n,表示操作的个数,下面 n 行每行有两个数 opt 和 x 表示操作的序号( 1≤opt≤6 )

输出格式

对于操作 3,4,5,6 每行输出一个数,表示对应答案

输入输出样例

输入 #1

10
1 106465
4 1
1 317721
1 460929
1 644985
1 84185
1 89851
6 81968
1 492737
5 493598

输出 #1

106465
84185
492737

说明/提示

【数据范围】

对于 100% 的数据, 1≤n≤105,∣x∣≤107

1.分析

Q1:我们需要那些准备呢?

A1:编程基础的语法,二分答案的思想(二叉搜索树)和手写4K的勇气。(别怕,怕你就输了)

Q2:我们需要用什么算法?

A2:没错,就是标题——伸展树。至于为什么叫Splay,个人理解是这样的

Q3:Splay的思想是什么?

A3:当然是伸展啦。qwq

Q4:Splay一下子学不会怎么办?

A4:正常,请保持耐心,本人打了5遍的Splay还可以打出WA和TLE。我在学Splay的时候适当的背了一点代码(因为有些代码真的较难理解)。

01).定义重要

#include<bits/stdc++.h>

#define Root T[0].Ch[1]//Root为0结点的右儿子(左儿子也没事,但后面代码的时候稍微注意一下)

struct Floor{//用T来表示伸展树结点的基本信息
	int Val;//结点的值
	int Fa;//结点的父亲
	int Ch[2];//结点的左右儿子
	int Size;//以此结点为根的子树的大小
	int Recy;//关键,结点的重复次数
}T[MAXN];

int N;//操作个数
int NodeNum;//申请的结点个数
int TSize;//伸展树的大小


02).更新函数(Update)

void Update(int X){
	T[X].Size=T[T[X].Ch[0]].Size+T[T[X].Ch[1]].Size+T[X].Recy;//当前的结点为根的子树大小为左右儿子的大小加上自己的重复次数
}

03).父子关系函数(Relation)

int Relation(int X){//求X为它父亲的什么儿子(左/右)
	return T[T[X].Fa].Ch[0]==X? 0:1;//0为左儿子,1为右儿子
}

04).连接函数(Connect)

void Connect(int X,int Y,int Son){//Y当父亲,X当儿子,Son表示X要成为Y的什么儿子(左/右)(千万别把第三个参数忘记)
	T[X].Fa=Y;//X认Y做父亲
	T[Y].Ch[Son]=X;//Y认X做(左/右)儿子
}

05).旋转函数(Rotate)关键

我相信许多同学(包括我自己),都被旋转搞晕过。其实这都要靠套路。

我们旋转的时候是以黄色作为我们的旋转点的。
而我们的目标是让黄色上去。
观察如上图片,我们可以发现,只有结点红,橙,黄,蓝的父子关系发生变化。只有结点橙,黄的Size发生变化。
我们看到,蓝色的位置是处于黄色与橙色相夹的位置,一旦黄色向上旋转,必然从黄色的左(右)儿子变成橙色的右(左)儿子。
第一步,我们先让黄色把蓝色吸过来,吸过来占据原来黄色在橙色当儿子的位置。
然后我们要把黄色转上去,所以让黄色吸来占据原来蓝色在黄色当儿子的位置。
最后,我们要让红色认黄色这个儿子,所以让黄色来占据原来橙色在红色当儿子的位置。
这就是旋转三板斧,理清楚之后就非常的简单了。

void Rotate(int X){
	int Y=T[X].Fa;
	int YSon=Relation(X);
	int R=T[Y].Fa;
	int RSon=Relation(Y);
	int XSon=YSon^1;
	int B=T[X].Ch[XSon];
	Connect(B,Y,YSon);//旋转三板斧
	Connect(Y,X,XSon);
	Connect(X,R,RSon);
	Update(Y);//别忘了哦
	Update(X);
}

06).伸展函数(Splay)关键

伸展树最关键的来啦
我们在旋转伸展树时,会有一些结点深度变浅,但是也会有一些结点深度变深。貌似双旋可以让树的深度更加平均。

void Splay(int From,int To){//表示结点From转到To的位置
	To=T[To].Fa;//问题交给大家Q5:为什么要写这一步
	while(T[From].Fa!=To){//如果From未到To
		int Up=T[From].Fa;
		if(T[Up].Fa==To){
			Rotate(From);
		}else if(Relation(Up)==Relation(From)){
			Rotate(Up);
			Rotate(From);
		}else{
			Rotate(From);
			Rotate(From);
		}
	}
}

A5->Q5:当From为To的儿子时,Rotate操作会把To给转下去。

07).查找结点函数(Find)

int Find(int Value){
	int Now=Root;//从根节点开始
	while(Now){//Now=0表示没有这个结点了
		if(Value==T[Now].Val){
			Splay(Now,Root);//这一步千万别忘,不然会像我一样光荣WA
			return Now;//返回结点编号
		}
		int Next=Value<T[Now].Val? 0:1;//Q6:这一步能看懂吗
		Now=T[Now].Ch[Next];
	}
	return 0;
}

A6->Q6:如果Value<T[Now].Val就往左走,否则往右走。如果忘了,可以翻上去再看一下 1).定义哦。

08).创造结点(CreNode)

void CreNode(int Value,int Father){
	NodeNum++;//结点数量加1
	T[NodeNum].Fa=Father;//认父亲
	T[NodeNum].Val=Value;//赋值
	T[NodeNum].Size=T[NodeNum].Recy=1;//注意:不要忘记给Recy赋值为1哦
}

09).创造结点(Insert)重要

int Insert(int Value){
	
	if(TSize==0){
		TSize++;
		Root=NodeNum+1;//别忘这一步!!!
		CreNode(Value,0);//创造结点
		return NodeNum;//Q7:返回值,至于为什么返回,下一个函数解答
	}else{
		TSize++;
		int Now=Root;//从根节点开始
		while(1){
			T[Now].Size++;//别忘这一步,每一次Now走到的地方必然是新加结点的长辈或是它自己
			if(Value==T[Now].Val){//如果找到值一样的,就Recy加一
				T[Now].Recy++;
				return Now;//返回结点编号
			}
			int Next=Value<T[Now].Val? 0:1;//还记得Q6吗,现在理解了吗,如果没有,就再向前翻一下 07).查找结点函数
			if(!T[Now].Ch[Next]){//发现不存在结点
				T[Now].Ch[Next]=NodeNum+1;//这一步别忘,父子关系要认清
				CreNode(Value,Now);//创造结点
				return NodeNum;//返回结点编号
			}
			Now=T[Now].Ch[Next];//移动Now
			
		}
	}

}

10).创造结点(Push)

void Push(int Value){
	int AddNode=Insert(Value);
	Splay(AddNode,Root);
}

A7->Q7:别忘了还要Splay呀。当然也可以不写这个函数,直接用Splay(NodeNum,Root);代替09).创造结点的return NodeNum;

11).破坏结点(Destroy)

void Destroy(int X){
	T[X].Val=T[X].Fa=T[X].Ch[0]=T[X].Ch[1]=T[X].Size=T[X].Recy=0;//完全把这个结点去除
}

12).破坏结点(Pop)重要

void Pop(int Value){
	int KillNode=Find(Value);//还记得Find里面有一句Splay(Now,Root);所以现在KillNode是整个伸展树的根,或没有这个结点
	if(!KillNode) return;//没有这个结点就退
	TSize--;//别忘记把整个树的大小减一
	if(T[KillNode].Recy>1){//
		T[KillNode].Recy--;
		T[KillNode].Size--;//别漏哦,X的Size是它左右儿子的Size加上Recy。如果忘了,可以向前翻 02).更新函数
		return; 
	}
	if(!T[KillNode].Ch[0]){如果没有左子树,直接右子树的根代替
		Root=T[KillNode].Ch[1];
		T[Root].Fa=0;//别漏哦,父子关系要认清
	}else{//如果有左子树
		int LMAX=T[KillNode].Ch[0];//在左子树找最大的结点来代替
		while(T[LMAX].Ch[1]){
			LMAX=T[LMAX].Ch[1];
		}
		Splay(LMAX,T[KillNode].Ch[0]);//把它转到左子树的根
		int R=T[KillNode].Ch[1];//右子树的根
		Connect(R,LMAX,1);//连接,R是LMAX的右儿子
		Connect(LMAX,0,1);//连接,如果最开始写Root为T[0].Ch[0],这边就写Connect(LMAX,0,0);
		Update(LMAX);//这一步也别忘记
	}
	Destroy(KillNode);//破坏这个结点
	
}

13).查找值为X的排名(Rank)困难

int Rank(int Value){
	int Now=Root;//从根开始
	int Ans=1;//它应该至少是第一名吧
	while(Now){
		if(Value==T[Now].Val){//找到了
			Ans=Ans+T[T[Now].Ch[0]].Size;//别忘这一步,它会大于左边的所有结点值
			Splay(Now,Root);//旋转到根上
			return Ans;//返回排名
		}else
		if(Value<T[Now].Val){//向左
			Now=T[Now].Ch[0];
		}else
		if(Value>T[Now].Val){//向右
			Ans=Ans+T[T[Now].Ch[0]].Size+T[Now].Recy;//关键,向右时Ans要加上左边的Size和当前的Recy,Q8:为什么要这么加?
			Now=T[Now].Ch[1];
		}
		
	}
	return 0;
}

A8->Q8:因为根据二叉排序树的性质,如果Value>T[Now].Val,它会大于所有左边的结点值,同时也大于T[X].Recy所代表的值。

14).查找排名为X的值(Atrank)困难

int Atrank(int X){ 
	if(X>TSize){//如果排名大于整个树的大小,就意味着没有这个值
		return 0;
	}
	int Now=Root;//从根开始
	while(Now){
		if(T[T[Now].Ch[0]].Size<X && X<=T[Now].Size-T[T[Now].Ch[1]].Size){//解释:T[Now].Size-T[T[Now].Ch[1]].Size等同于T[T[Now].Ch[0]].Size+T[Now].Recy表示小于等于T[Now].Val的结点总个数。Q9:为什么这么比较
			Splay(Now,Root);//旋转到根
			return T[Now].Val;//返回结点值
		}
		if(X<=T[T[Now].Ch[0]].Size){//比左边的Size还要小,就往左走
			Now=T[Now].Ch[0];
		}else
		if(T[Now].Size-T[T[Now].Ch[1]].Size<X){//翻译:排名X的值比T[Now].Val大,则向右走
			X=X-(T[Now].Size-T[T[Now].Ch[1]].Size);//一定要减,千万别忘
			Now=T[Now].Ch[1];
		}
	}
}

A9->Q9:因为X大于当前左子树的所有值,但X小于等于小于等于T[Now].Val的结点总个数。所以X的值就是T[Now].Val。(这个是真的难讲,如果真的不会,可以先暂时背一下代码)

15).查找值为X的前驱(Lower)(与13相似)

int Lower(int Value){
	int Now=Root;
	int Ans=-INF;//找最大值,Ans为负无穷
	while(Now){
		if(Value>T[Now].Val && T[Now].Val>Ans){//符合比Value小,而且比当前答案大,就更新
			Ans=T[Now].Val;
		}
		if(Value>T[Now].Val){//重要,这容易写错。如果Value==T[Now].Val,就要向左走
			Now=T[Now].Ch[1];
		}else
		if(Value<=T[Now].Val){
			Now=T[Now].Ch[0];
		}
	}
	return Ans;//返回前驱
}

16).查找值为X的后继(Upper)(与12相似)

int Upper(int Value){
	int Now=Root;
	int Ans=INF;//找最小值,Ans为正无穷
	while(Now){
		if(Value<T[Now].Val && T[Now].Val<Ans){//符合比Value大,而且比当前答案小,就更新
			Ans=T[Now].Val;
		}
		if(Value<T[Now].Val){//重要,这容易写错。如果Value==T[Now].Val,就要向右走
			Now=T[Now].Ch[0];
		}else
		if(Value>=T[Now].Val){
			Now=T[Now].Ch[1];
		}
	}
	return Ans;//返回后继
}

17).主程序(Main)

int main(){
	scanf("%d",&N);//输入N
	while(N--){
		scanf("%d%d",&Opt,&X);//输入指令和值
		if(Opt==1){//分指令进行操作
			Push(X);//添加结点操作。
		}else if(Opt==2){
			Pop(X);//删除结点操作。
		}else if(Opt==3){
			printf("%d\n",Rank(X));//输出值为X的排名。
		}else if(Opt==4){
			printf("%d\n",Atrank(X));//输出第X小的数。
		}else if(Opt==5){
			printf("%d\n",Lower(X));//输出值为X的前驱。
		}else{
			printf("%d\n",Upper(X));//输出值为X的后继。
		}
	}
	return 0;//结束
}

悄悄说一句话,把上面所有的代码拼接起来就可以过洛谷平衡树模板题。

posted @ 2020-05-07 16:59  eromangasensei  阅读(331)  评论(1编辑  收藏  举报