• 浅谈伸展树


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

    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
    ",Rank(X));//输出值为X的排名。
    		}else if(Opt==4){
    			printf("%d
    ",Atrank(X));//输出第X小的数。
    		}else if(Opt==5){
    			printf("%d
    ",Lower(X));//输出值为X的前驱。
    		}else{
    			printf("%d
    ",Upper(X));//输出值为X的后继。
    		}
    	}
    	return 0;//结束
    }
    

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

  • 相关阅读:
    internet连接共享被启用时 出现了一个错误 (null)
    mybatis01-1测试
    配置没有问题,虚拟机Ubuntu系统ifconfig没有网卡信息
    Ubuntu启动Apache
    VM虚拟机Linux系统eth0下面没有inet和inet6
    jQuery通过id和name获取值的区别
    1.4.3 ID遍历爬虫(每天一更)
    mysql中的SQL语句执行的顺序
    Mecanim动画系统丶
    html中常见的行内元素和块级元素,还有常见的行内块元素
  • 原文地址:https://www.cnblogs.com/eromangasensei/p/12844141.html
Copyright © 2020-2023  润新知