平衡树笔记 2025.2.6

研究一个不高深的东东--平衡树!!


  • 平衡树分很多种,今天学的是Splay(伸展树)--就是通过树上Splay操作让某个节点来到根的位置,但 树保持平衡(左小右大)

  • Splay 树是一棵二叉搜索树,查找某个值时满足性质:左子树任意节点的值 < 根节点的值 < 右子树任意节点的值。


  • 一般维护

    1. Son[pos][2] pos节点的两个儿子编号
    
    2. Fa[pos] pos节点的父亲编号
    
    3.Size[pos] pos节点的子树大小(包括pos)
    
    4.Val[pos] pos节点的权值
    
    5.Cnt[pos] val[pos]的出现次数
    
    6.Root 根节点
    
    7.Sum 节点编号(每次有新节点就++)
    

  • 旋转 (Rotate)

    辅助函数

    1.Get

    bool Get(int u){return u==Son[Fa[u]][1];}
    //Son数组记录的是左右儿子
    //Son[Fa[u]][1]就是u父亲的右儿子
    //如果u就是右儿子 (u==Son[Fa[u]][1])的逻辑值为1
    //u为左儿子 返回0  Son[][1]代表右儿子 Son[][0] 代表左儿子
    

    2.Update

    void Update(int u){
    	Size[u]=Cnt[u];//首先是自己的出现次数包含在子树大小中
    	if(Son[u][1]) Size[u]+=Size[Son[u][1]];//有右儿子就+右儿子大小
    	if(Son[u][0]) Size[u]+=Size[Son[u][0]];//有左儿子就+左儿子大小
    }
    
    1. 考虑开始旋转

    旋转分为两种 左旋(zig) 右旋(zag)

    由于要满足平衡--左小右大

    借助图片 点即为点权 左到右是将节点2旋到节点1处

    上代码!

    void Rotate(int u){
    	//Ft 父亲 Gft 父亲的父亲 
    	int Ft=Fa[u],Gft=Fa[Ft];
    	//判断节点u是左子还是右子 
    	bool Wson=Get(u);
    	//将节点 Ft(1) 的 Wson(左儿子) 变成要旋转节点 u(2)
        //的另一个儿子(Wson^1)
        //把 4 变成 1 的左儿子
    	Son[Ft][Wson]=Son[u][Wson^1];
        //把 4 的父亲从 2 变成 1
    	Fa[Son[Ft][Wson]]=Ft;
        // 1 变成 u(2) 的儿子
    	Son[u][Wson^1]=Ft;
        // u 成为 u 以前父亲的父亲(大雾)
    	Fa[Ft]=u;
        // u 成为 u 以前父亲父亲的儿子
    	Fa[u]=Gft;
        // 当Gft不是根 将 u 变成Gft的儿子
    	if(Gft!=0) Son[Gft][Ft==Son[Gft][1]]=u;
    	//结合图片理解 
    	if(u!=0) Update(u);
    	if(Ft!=0) Update(Ft); 
    }
    

  • Splay

    其实就是把一个节点旋转到根

    考虑三种情况

    1. x节点差一下就可以到达根

    那就单旋转一次 Rotate(x)

    2.x节点要连续旋转两次

    emm 那就旋转两次

    3.x要一左一右旋转两次

    转一次x,转一次p

    //Root 当前的根 
    int Root;
    //Splay 将节点u旋转至根 
    void Splay(int u){
    //当u在Root下面,Fa[u]=0,结束循环
    	for(int Ft;Ft=Fa[u];Rotate(u))
    		if(Fa[Ft]!=0) Rotate((Get(u)==Get(Ft))?Ft:u);
    	//双旋转
    	//更新根节点 
    	Root=u;
    }
    

  • 智慧例题P3369

    维护一个数据结构,使其可以完成

      1.加入值为 x 的成员
    
      2.删除一个值为 x 的成员
    
      3.查询有多少数比 x 小
    
      4.权值从小到大排名为 x 的成员
    
      5.查询小于 x,且最大的成员的值
    
      6.查询大于 x,且最小的成员的值
    

    一个个来

    我都在讲这么久Slpay——Tree了,你说写什么?

    首先 Splay_Tree可以在 中完成1,2,3,4,5,6

    然后Splay_Tree写起来比较简洁

    所以 这里介绍Splay_Tree的写法

  • [1.加入值为 x 的成员 ]

    A.

    如果树空了,则直接插入根并退出。

    B.

    如果当前节点的权值等于 k 则增加当前节点的大小并更新节点和父亲的信息,将当前节点进行 Splay 操作

    C.

    否则按照二叉查找树的性质向下找,找到空节点就插入即可

    //Sum 当前平衡树中有多少节点 
    int Sum;
    //插入点权为u的点 
    void Insert(int u){
    	//根为0,即当前树上没有节点 
    	if(Root==0){
    		//节点数量+1 
    		Sum++;
    		//初始化一下 
    		Son[Sum][0]=Son[Sum][1]=Fa[Sum]=0;
    		//根节点就是一号节点 
    		Root=Sum;
    		//一号节点的子树与u值出现次数为1 
    		Size[Sum]=Cnt[Sum]++;
    		//给予点权 
    		Val[Sum]=u;
    		return ;
    	}
    	//找可以插入u的位置
    	//从根开始向下找 
    	int Now=Root,Ft=0;
    	for(int i=1;i>=0;i++){
    		//Now节点的值与u一样 
    		if(u==Val[Now]){
    			//出现次数++ 
    			Cnt[Now]++;
    			//更新 
    			if(Now!=0) Update(Now);
    			if(Ft!=0) Update(Ft);
    			//将now旋转到根处 
    			Splay(Now);
    			break;
    		}
    		Ft=Now;
    		//向下找可能的插入点 
    		Now=Son[Now][Val[Now]<u];
    		//到达叶子节点
    		//要新开一个点 
    		if(Now==0){
    			//节点编号为Sum+1 
    			Sum++;
    			//初始化 
    			Son[Sum][0]=Son[Sum][1]=0;
    			Fa[Sum]=Ft;
    			Size[Sum]=Cnt[Sum]=1;
    			Son[Ft][Val[Ft]<u]=Sum;
    			Val[Sum]=u;
    			if(Ft!=0) Update(Ft);
    			//旋转到根 
    			Splay(Sum);
    			break;
    		}
    	}
    }
    
  • [3.查询有多少数比 x 小]

    A.

    如果 x 比当前节点的权值小,向其左子树查找

    B.

    如果 x 比当前节点的权值大,将答案加上左子树(size)和当前节点(cnt)的大小,向其右子树查找

    C.

    如果 x 与当前节点的权值相同,将答案加 1 并返回

    //找点权为u的点排名 
    int Find_Rank(int u){
    	int Now=Root,Ans=0;
    	for(int i=1;i>=0;i++){
    		//如果Now点的值比u大
    		//向左走 
    		if(u<Val[Now]) Now=Son[Now][0];
    		else{
    			//加上全部now的右子树的size值 
    			Ans+=(Son[Now][0]?Size[Son[Now][0]]:0);
    			//u点值刚刚好
    			//题意为Ans+1 
    			if(u==Val[Now]){
    				Splay(Now);
    				return Ans+1;
    			}
    			//向右儿子走 
    			Ans+=Cnt[Now];
    			Now=Son[Now][1];
    		}
    	}
    }
    
  • [4.权值从小到大排名为 x 的成员]

    A.

    如果左子树非空且剩余排名 k 不大于左子树的大小 size,那么向左子树查找

    B.

    否则将 k 减去左子树的和根的大小。如果此时 k 的值小于等于 0,则返回根节点的权值,否则继续向右子树查找。

    //找点权为u的点排名 
    int Find_Rank(int u){
    	int Now=Root,Ans=0;
    	for(int i=1;i>=0;i++){
    		//如果Now点的值比u大
    		//向左走 
    		if(u<Val[Now]) Now=Son[Now][0];
    		else{
    			//加上全部now的右子树的size值 
    			Ans+=(Son[Now][0]?Size[Son[Now][0]]:0);
    			//u点值刚刚好
    			//题意为Ans+1 
    			if(u==Val[Now]){
    				Splay(Now);
    				return Ans+1;
    			}
    			//向右儿子走 
    			Ans+=Cnt[Now];
    			Now=Son[Now][1];
    		}
    	}
    }
    
    
  • [ 6.查询大于 x,且最小的成员的值]

    [5.查询小于 x,且最大的成员的值 ]

    其实就是查找前驱与后继

    A.

    前驱定义为小于 x 的最大的数,那么查询前驱可以转化为:将 x 插入(此时 x 已经在根的位置了),前驱即为 x 的左子树中最右边的节点,最后将 x 删除即可。

    B.

    后继定义为大于 x 的最小的数,查询方法和前驱类似:x 的右子树中最左边的节点。

    int Find_From(){
    	int Now=Son[Root][0];
    	while(Son[Now][1]) Now=Son[Now][1];
    	return Now;
    }
    int Find_Neut(){
    	int Now=Son[Root][1];
    	while(Son[Now][0]) Now=Son[Now][0];
    	return Now;
    }
    
    
  • [2.删除一个值为 x 的成员 ]

    放个模板,先鸽着,有缘再更

    void Delete(int u){
    	int Ribbish=Find_Rank(u);
    	if(Cnt[Root]>1){
    		Cnt[Root]--;
    		if(Root!=0) Update(Root);
    		return ;
    	}
    	if(Son[Root][1]==Son[Root][0]&&Son[Root][0]==0){
    		Clear(Root);
    		Root=0;
    		return ;
    	}
    	if(Son[Root][0]==0){
    		int Fi_Root=Root;
    		Root=Son[Root][1];
    		Fa[Root]=0;
    		Clear(Fi_Root);
    		return ;
    	}
    	if(Son[Root][1]==0){
    		int Fi_Root=Root;
    		Root=Son[Root][0];
    		Fa[Root]=0;
    		Clear(Fi_Root);
    		return ;
    	}
    	int Left_Mau=Find_From();
    	int Fi_Root=Root;
    	Splay(Left_Mau);
    	Son[Root][1]=Son[Fi_Root][1];
    	Fa[Son[Fi_Root][1]]=Root;
    	Clear(Fi_Root);
    	if(Root!=0) Update(Root);
    }
    

emm 补充一个辅助函数

//Clear 将编号为u的点清空删除 
void Clear(int u){Son[u][0]=Son[u][1]=Fa[u]=Size[u]=Cnt[u]=Val[u]=0;}
附上全部代码
  #include<bits/stdc++.h>
  #define int long long
  using namespace std;
  const int M=3e5+110;
  int Read(){
  	int sum=0,k=1;
  	char c=getchar();
  	while(c>'9'||c<'0'){
  		if(c=='-') k=-1;
  		c=getchar();
  	}
  	while(c>='0'&&c<='9'){
  		sum=sum*10+c-48;
  		c=getchar();
  	}return sum*k;
  }
  int Son[M][2];
  int Fa[M],Size[M],Cnt[M],Val[M];
  void Clear(int u){Son[u][0]=Son[u]  [1]=Fa[u]=Size[u]=Cnt[u]=Val[u]=0;}
  bool Get(int u){return u==Son[Fa[u]][1];}
  void Update(int u){
  	Size[u]=Cnt[u];
  	if(Son[u][1]) Size[u]+=Size[Son[u][1]];
  	if(Son[u][0]) Size[u]+=Size[Son[u][0]];
  }
  void Rotate(int u){
  	int Ft=Fa[u],Gft=Fa[Ft];
  	bool Wson=Get(u);
  	Son[Ft][Wson]=Son[u][1-Wson];
  	Fa[Son[Ft][Wson]]=Ft;
  	Son[u][1-Wson]=Ft;
  	Fa[Ft]=u;
  	Fa[u]=Gft;
  	if(Gft!=0) Son[Gft][Ft==Son[Gft][1]]=u;
  	if(u!=0) Update(u);
  	if(Ft!=0) Update(Ft); 
  }
  int Root;
  void Splay(int u){
  	for(int Ft;Ft=Fa[u];Rotate(u))
  		if(Fa[Ft]!=0) Rotate((Get(u)==Get(Ft))?Ft:u);
  	Root=u;
  }
  int Sum;
  void Insert(int u){
  	if(Root==0){
  		Sum++;
  		Son[Sum][0]=Son[Sum][1]=Fa[Sum]=0;
  		Root=Sum;
  		Size[Sum]=Cnt[Sum]++;
  		Val[Sum]=u;
  		return ;
  	}
  	int Now=Root,Ft=0;
  	for(int i=1;i>=0;i++){
  		if(u==Val[Now]){
  			Cnt[Now]++;
  			if(Now!=0) Update(Now);
  			if(Ft!=0) Update(Ft);
  			Splay(Now);
  			break;
  		}
  		Ft=Now;
  		Now=Son[Now][Val[Now]<u];
  		if(Now==0){
  			Sum++;
  			Son[Sum][0]=Son[Sum][1]=0;
  			Fa[Sum]=Ft;
  			Size[Sum]=Cnt[Sum]=1;
  			Son[Ft][Val[Ft]<u]=Sum;
  			Val[Sum]=u;
  			if(Ft!=0) Update(Ft);
  			Splay(Sum);
  			break;
  		}
  	}
  }
  int Find_Num(int u){
  	int Now=Root;
  	for(int i=1;i>=0;i++){
  		if(Son[Now][0]&&u<=Size[Son[Now][0]])
  			Now=Son[Now][0];
  		else{
  			int Temp=(Son[Now][0]?Size[Son[Now][0]]:0)+Cnt[Now];
  			if(u<=Temp) return Val[Now];
  			u-=Temp;
  			Now=Son[Now][1];
  		}
  	}
  }
  int Find_Rank(int u){
  	int Now=Root,Ans=0;
  	for(int i=1;i>=0;i++){
  		if(u<Val[Now]) Now=Son[Now][0];
  		else{
  			Ans+=(Son[Now][0]?Size[Son[Now][0]]:0);
  			if(u==Val[Now]){
  				Splay(Now);
  				return Ans+1;
  			}
  			Ans+=Cnt[Now];
  			Now=Son[Now][1];
  		}
  	}
  }
  int Find_From(){
  	int Now=Son[Root][0];
  	while(Son[Now][1]) Now=Son[Now][1];
  	return Now;
  }
  int Find_Neut(){
  	int Now=Son[Root][1];
  	while(Son[Now][0]) Now=Son[Now][0];
  	return Now;
  }
  void Delete(int u){
  	int Ribbish=Find_Rank(u);
  	if(Cnt[Root]>1){
  		Cnt[Root]--;
  		if(Root!=0) Update(Root);
  		return ;
  	}
  	if(Son[Root][1]==Son[Root][0]&&Son[Root][0]==0){
  		Clear(Root);
  		Root=0;
  		return ;
  	}
  	if(Son[Root][0]==0){
  		int Fi_Root=Root;
  		Root=Son[Root][1];
  		Fa[Root]=0;
  		Clear(Fi_Root);
  		return ;
  	}
  	if(Son[Root][1]==0){
  		int Fi_Root=Root;
  		Root=Son[Root][0];
  		Fa[Root]=0;
  		Clear(Fi_Root);
  		return ;
  	}
  	int Left_Mau=Find_From();
  	int Fi_Root=Root;
  	Splay(Left_Mau);
  	Son[Root][1]=Son[Fi_Root][1];
  	Fa[Son[Fi_Root][1]]=Root;
  	Clear(Fi_Root);
  	if(Root!=0) Update(Root);
  }
  signed main(){
  	int m=Read();
  	for(int i=1;i<=m;i++){
  		int oop=Read(),u=Read();
  		if(oop==1){
  			Insert(u);
  			continue;
  		}
  		if(oop==2){
  			Delete(u);
  			continue;
  		}
  		if(oop==3){
  			Insert(u);
  			printf("%lld\n",Find_Rank(u));
  			Delete(u);
  			continue;
  		}
  		if(oop==4){
  			printf("%lld\n",Find_Num(u));
  			continue;
  		}
  		if(oop==5){
  			Insert(u);
  			printf("%lld\n",Val[Find_From()]);
  			Delete(u);
  			continue;
  		}
  		if(oop==6){
  			Insert(u);
  			printf("%lld\n",Val[Find_Neut()]);
  			Delete(u);
  			continue;
  		}
  	}
  	return 0;
  }
  /*
  10
  1 106465
  4 1
  1 317721
  1 460929
  1 644985
  1 84185
  1 89851
  6 81968
  1 492737
  5 493598
  */
溜溜 886~

  • [ 时间复杂度证明]
  1. 每次操作rotate ,由于Splay_Tree是一棵二叉树,如果有个节点,最多有层每次上升一层可看作,所以Splay就是级别

  2. 插入,查询,删除等操作,只有在最后才会执行Splay,也可以认为是

  3. 执行m次,m n同阶,总的可以认为是

4.那么每次可以认为是


然后有了Slapy,就能讲讲FHQ

**

  1. ------

    FHQ是一种没有旋转操作的Treap,由神犇范浩强提出的,功能比 Treap强大,无旋 treap 又称分裂合并 treap。它仅有两种核心操作, 即为分裂与合并。通过这两种操作,在很多情况下可以比旋转 treap 更 方便的实现别的操作。(支持可持久化、区间操作)

2.维护以下

    用结构体;
    struct Tree_Node{
        int lz , rz;
        
    }
全部评论

相关推荐

不愿透露姓名的神秘牛友
07-04 14:23
steelhead:你回的有问题,让人感觉你就是来学习的
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务