首页 > 其他分享 >平衡树专题Splay

平衡树专题Splay

时间:2024-07-02 15:34:21浏览次数:19  
标签:splay 专题 int son Splay 平衡 now root

写在前面: 部分来自孙宝(@Steven24)的博客,表示感谢。

认识

什么是Splay

就是BST的一种,整体效率是很高的,均摊的次数是O(logn)级别的。

基本操作就是把节点旋转到BST的root,从而改善BST的平衡性,但是很多人会在旋转中转晕

建议找个动图看看,或是上B站找个几分钟的视频看看就理解了。

烧烤

如何可以把一个节点旋转到BST的root的地方,这是Splay的核心,为了完成这个操作,我们主要是需要分成两步:

1.每旋转一次,就使得我们的目标节点x的层次上升一层,最后到达root层

2.在旋转完后,BST的平衡性被破坏了,这是毋庸置疑的,所以我们需要进行一系列的调整,使这棵BST重新回到平衡状态

显然,如果只考虑1,那么使用Treap树的旋转法即可,每次x与x的父亲交换位置(x上升一层)。可Treap树的这种“单旋”并不能减少BST的层数。

所以我们就升级一下,再拉来一个更能打的,祖父节点,让这三代人一起转。

Splay旋转

#define left 0,right 1

这个就是LL,RR,LR,RL四个Splay的基本模型

网上有很多

自己看吧

我推荐这个,因为我就是看着他弄懂的基础知识

Splay四种模型

Splay操作

由于支持单点旋转改变平衡因子的特性Splay常用于处理区间分裂和合并问题。

例如:一个常见的区间操作,修改或查询区间[L,R],用Splay树就很容易实现:先把L-1旋转到根,然后把节点R+1旋转到L-1的右子树上,此时,L+1的左子树就是区间[L,R]。

1.旋转:

rotate(x)对x点进行一次旋转,若x是一个右儿子,左旋,反之亦反之。

Splay Rotate

void rotate(int x)//单旋一次
{
	int f=t[x].fa;//f:父亲
	int g=t[f].fa;//g:祖父
	int son=get(x);
	if(son==1)//x是左儿子,右旋
	{
		t[f].rs=t[x].ls;
		if(t[f].rs)
		{
			t[t[f].rs].fa=f;
		}
	}
	else//x是右儿子,左旋
	{
		t[f].ls=t[x].rs;
		if(t[f].ls)
		{
			t[t[f].ls].fa=f;
		}
	}
	t[f].fa=x;//x旋为f的父节点
	if(son==1)//左旋,f变为x的左儿子
	{
		t[x].ls=f;
	}
	else//右旋,f变为x的右儿子
	{
		t[x].rs=f;
	}
	t[x].fa=g;//x现在是祖父的儿子
	if(g)//更新祖父的儿子
	{
		if(t[g].rs==f)
		{
			t[g].rs=x;
		}
		else
		{
			t[g].ls=x;
		}
	}
	Update(f);
	Update(x);
}

Splay(int x,int goal),把节点x旋转到goal位置。goal=0表示把x旋转到根,x是新的根。goal≠0表示把x旋转为goal的儿子。

Splay Rotate Destiny
 void Splay(int x,int goal)
{
	if(goal==0)
	{
		root=x;
	}
	while(1)
	{
		int f=t[x].fa;//一次处理x,f,g三个节点
		int g=t[f].fa;
		if(f==goal){
			break;
		}
		if(g!=goal)//有祖父,分为一字旋和之字旋两种情况
		{
			if(get(x)==get(f))//一字旋,先旋转f,g
			{
				rotate(f);
			}
			else//之字旋,直接旋转x
			{
				rotate(x);
			}
		}
		rotate(x);
	}
	Update(x);
} 

2.分裂和合并:

Insert()、Del()函数中包含了分裂与合并,详情见代码注释。利用Splay函数实现分裂与合并,编码很简单。

Splay Insert and Delete
 void Insert(int L,int len)//插入一段区间
{
	int x=kth(root,L);//x为第L个数的位置,y为第L+1个数的位置
	int y=kth(root,L+1);
	Splay(x,0);//分裂
	Splay(y,x);
    //先把x旋转到根,然后把y旋转到x的儿子,且y的儿子为空
	t[y].ls=build(1,len,y);//合并:建一棵树,挂到y的左儿子上
	Update(y);
	Update(x);
}
void Del(int L,int R)//删除区间[L+1,R]
{
	int x=kth(root,L);
	int y=kth(root,R+1);
	Splay(x,0);//y是x的右儿子,y的左儿子是待删除的区间
	Splay(y,x);
	t[y].ls=0;//剪短左子树,等于直接删除,这里为了简单,没有释放空间
	Update(y);
	Update(x);
}

3.查询:

Splay Find
 int kth(int x) { //查询排名为x的数 
	int now = root;
	while (1) {
		if (siz[son[now][0]] >= x) now = son[now][0]; //查过头了
		else if (siz[son[now][0]] + cnt[now] >= x) return val[now]; //正好查到
		else {
			x -= (siz[son[now][0]] + cnt[now]);
			now = son[now][1]; //没查够 
		} 
	}
}
Splay Rank
 int rank(int x) { //查询x的排名 
	int now = root, ans = 0;
	while (1) {
		if (!now) return ans + 1; //树中不存在这个数 那就是目前查到比它小的数的数目+1
		if (val[now] > x) now = son[now][0]; //要查的数比now节点的数小 说明在左子树
		else if (val[now] == x) {
			ans += siz[son[now][0]]; //比它小的数的数目
			splay(now);
			return ans + 1; 
		} else { //要查的数在左子树 
			ans += siz[son[now][0]] + cnt[now]; //此时当前节点和左子树所有数都比它小 
			now = son[now][1];
		}
	} 
}
Splay Find pre/nxt
 int findpre() { //查询根节点的前驱对应的结点编号 
	int now = son[root][0]; //首先进入左子树 因为左子树的所有数都比它小 
	while (son[now][1]) now = son[now][1]; //然后一直往大的走 找最大的
	return now; 
} 

int findnxt() { //跟上面同理 
	int now = son[root][1];
	while (son[now][0]) now = son[now][0];
	return now;
}

···

else if (opt == 5) {
	splay.insert(x); //把x先旋到根节点 而且一定要插入而不是直接旋 防止树上本来就没有x
	printf("%d\n", splay.val[splay.findpre()]);
	splay.del(x); //用完删掉 
} else {
	splay.insert(x); //同上 
	printf("%d\n", splay.val[splay.findnxt()]);
	splay.del(x);
}

注意

(感谢孙宝)

所以 splay 容易写挂的原因找到了 要是哪个 splay 和 maintain 忘写了就寄了
那么怎么记住到底哪要写哪不用写呢

对于 maintain 很简单 改了啥就把它自己 maintain 一下 再把父亲(如果有)也 maintain 一下
对于 splay 实际上我们使用这个函数的同时如果要实现提根的功能 那就写
比如 insert 我们查询前驱后继需要使用它并且把插入的数旋到根 所以要写
比如 rank 我们就是要用它把权值为 x 的结点旋到根节点 所以要写
再比如 del 呃这个你肯定不能忘

如果还记不住怎么办呢

首先要明确 splay 是复杂度的保证 写少了复杂度就会假
但是写几个肯定是没事的
maintain 也是 如果该更新的没更新肯定就寄了
但是把没必要更新的也更新了问题就不大
所以实在不行这俩就是能写就写

当然这么干常数就又会变大 所以最好还是记一下上面那个

标签:splay,专题,int,son,Splay,平衡,now,root
From: https://www.cnblogs.com/MerlinForLee/p/18279939

相关文章

  • 平衡樹專題Treap
    前言:题单在此:HL平衡树0701-题单-洛谷|计算机科学教育新生态(luogu.com.cn)平衡树什么是平衡树?首先我们需要知道二叉查找树的内容。二叉查找树(BST:BinarySearchTree)首先,他是一棵二叉树其次,他的左子树的权值<根节点的权值<右子树的权值最后,也是最重要的,他的中序遍历......
  • Leetcode秋招冲刺(专题10--12)
    专题10:动态规划题目509:斐波那契数(NO)解题思路:动态五部曲动态五部曲:这里我们用一个一维数组来保存递归的结果确定dp数组以及下标的含义dp[i]的定义为:第i个数的斐波那契数值是dp[i]确定递推公式这道题已经把递推公式直接给了:状体转移方程dp[i]=dp[i-1]+dp[i-2];dp数......
  • 【K8s】专题六(3):Kubernetes 稳定性之自动扩缩容
    以下内容均来自个人笔记并重新梳理,如有错误欢迎指正!如果对您有帮助,烦请点赞、关注、转发!欢迎扫码关注个人公众号!一、基本介绍在Kubernetes中,自动扩缩容是一种动态调整集群资源,以灵活应对应用程序资源需求变化的机制。自动扩缩容可以分为两个层面:Node层面:根据业务规模......
  • 测试开发比,能代表质效平衡吗?
    看到一个很有意思的话题:测试团队需要保障质量,同时也要考虑测试效率,质量和效率之间的平衡,其实很大程度上取决于测试和开发的人数占比。只有先保证资源上的平衡,才能在保障质量的同时保证一定的测试效率。这个话题背后的逻辑成立吗?我仔细思考了这个问题,表面看似合乎逻辑,但经不起分......
  • 专题二:Spring源码编译
    目录下载源码配置Gradle配置环境变量配置setting文件配置Spring源码配置文件调整问题解决完整配置gradel.propertiesbuild.gradlesettiings.gradel在专题一:Spring生态初探中我们从整体模块对Spring有个整体的印象,现在正式从最基础的Spring模块进一步学习,第一步......
  • 【算法专题--栈】用队列实现栈 -- 高频面试题(图文详解,小白一看就懂!!)
    目录一、前言二、题目描述三、解题方法⭐两个队列实现栈......
  • 47、基于连续Hopfield神经网络的不稳定平衡
    1、连续Hopfield神经网络的不稳定平衡原理及流程连续Hopfield神经网络是一种用于模式识别和记忆的神经网络模型,其基本原理是通过权重矩阵来存储并检索各种模式。不稳定平衡指的是在Hopfield网络中,输入的模式通过网络的动态演化最终会达到一个平衡状态,该状态可能是存储的模式之......
  • Klipper RP2040 display ssd1306 0.96 屏幕配置
    接线屏幕接线parampinGNDGNDVCCVCCSCLSDA编码器接线parampinGNDGNDEN1VCCEN2CLklipper配置#显示屏及旋钮[display]lcd_type:ssd1306#i2c_bus:i2c0dencoder_pins:^gpio24,^gpio23encoder_steps_per_detent:2c......
  • [题解]P2042 [NOI2005] 维护数列 - Splay解法
    P2042[NOI2005]维护数列一道思路不难,但实现细节很多的平衡树题,调了一天半终于做出来了w。对于初始序列,我们可以直接构建一条链(毕竟一个一个调用插入函数也可能形成一条链)。题解有递归直接构建成一棵严格平衡的二叉树的,这样也可以,常数可能会小一点。其中区间反转就是裸的文艺......
  • 智能优化算法应用:基于平衡优化器算法PID参数优化 - 附代码
    智能优化算法应用:基于平衡优化器算法PID参数优化-附代码文章目录智能优化算法应用:基于平衡优化器算法PID参数优化-附代码1.PID简介2.平衡优化器算法简介3.适应度函数设计4.算法实验与结果5.参考文献:6.Matlab代码摘要:本文主要介绍如何用平衡优化器算法进行PID参......