jjzjj

伸展树(Splay)详解

白简的博客~ 2023-03-28 原文

引入

在一条链中,二叉查找树的时间复杂度就会退化成 \(O(n)\),这时我们就需要平衡树来解决这个问题。

\(Splay\)(伸展树)是平衡树的一种,它的每一步插入、查找和删除的平摊时间都是 \(O(log_2 n)\),出于对编程复杂度和算法性能的考虑,这是 OI 中常用的算法。

性质

\(Splay\) 本质上还是对二叉查找树的优化。所以它也具备二叉查找树的性质,即左子树任意节点的值 \(<\) 根节点的值 \(<\) 右子树任意节点的值

操作

数组含义

root tot fa[i] ch[i][0] ch[i][1] val[i] size[i] cht[i]
根节点编号 节点数量 父节点编号 左儿子编号 右儿子编号 节点权值 子树大小 节点权值出现次数

基本操作

maintain(x):维护子树大小

void Splay::maintain(int x)
{
	size[x] = size[ch[x][0]] + size[ch[x][1]] + cnt[x];
	return ;
}

get(x):查询该节点是其父亲节点的左子树还是右子树

bool Splay::get(int x)
{
	if( x == ch[fa[x]][1] )
		return 1;
	return 0;
}

clear(x):清理该节点

void Splay::clear(int x)
{
	ch[x][0] = ch[x][1] = fa[x] = val[x] = size[x] = cnt[x] = 0;
	return ;
}

旋转

旋转操作实际上是让某一个节点上移一个位置。

旋转操作需要保证,二叉查找树的性质不会改变,节点维护的信息依然正确,\(root\) 必须指向旋转后的根节点。

若节点 x 是其父亲的左节点

由于 \(x\) 的右儿子的权值大于 \(x\) 的权值,且 \(x\) 及其子树都属于 \(y\) 的左子树(即 \(x\) 的右子树实际上小于 \(y\) 的权值),所以我们将 \(x\) 的右子树改为 \(y\) 的左子树。

  1. \(x\) 的右儿子变成 \(y\) 的左儿子,如果 \(x\) 有右儿子的话就让它的父亲变成 \(y\)
    ch[y][0] = ch[x][1]; fa[ch[x][1]] = y;

由于 \(y\) 及其子树的权值都大于 \(x\) 的权值,所以我们让 \(y\) 成为 \(x\) 的右儿子。

  1. 使 \(y\) 成为 \(x\) 的右儿子,\(x\) 变为 \(y\) 的父亲。
    ch[x][chk^1] = y; fa[y] = x;

  2. 如果 \(x\) 此时不是根节点,那么 \(x\) 将继承原先 \(y\) 作为 \(z\) 的儿子的位置(\(x\) 取代 \(y\) 成为 \(z\) 的左儿子或右儿子)。
    fa[x] = z; if(z) ch[z][y == ch[z][1]] = x;

由此我们得到了节点 \(x\) 上升一个位置的树,显然,这棵树仍然满足二叉搜索树的性质。

实现

void Splay::rotate(int x)
{
	int y = fa[x],z = fa[y],chk = get(x);
	
	ch[y][chk] = ch[x][chk ^ 1];
	
	if( ch[x][chk ^ 1] )
		fa[ch[x][chk ^ 1]] = y;
	ch[x][chk ^ 1] = y;
	
	fa[y] = x;
	fa[x] = z;
	
	if(z)
		ch[z][y == ch[z][1]] = x;
	
	maintain(y);
	maintain(x);// 别忘了维护子树大小 
	
	return ;
}

代码中采用异或来实现左右不同旋转情况,当然我们可以写两个函数分别来实现左旋和右旋。

伸展

伸展操作是在保持伸展树性质的前提下,将节点 \(x\) 转移到根节点。在这个转移过程中,我们分为三种情况

首先我们设节点 \(x\) 的父节点为节点 \(y\),若节点 \(y\) 有父节点,其父节点为 \(z\)

第一种情况:\(y\) 是根节点

  • \(x\)\(y\) 的左儿子,我们进行一次右旋操作
  • \(x\)\(y\) 的右儿子,我们进行一次左旋操作

第二种情况:\(y\) 不是根节点,且 \(x\)\(y\) 同为左儿子或右儿子

  • \(x\)\(y\) 同时是各自父节点的左儿子,则进行两次右旋操作
  • \(x\)\(y\) 同时是各自父节点的右儿子,则进行两次左旋操作

第三种情况:\(y\) 不是根节点,且 \(x\)\(y\) 一个为左儿子一个为右儿子

  • \(x\)\(y\) 的左儿子,\(y\)\(z\) 的右儿子,则进行一次右旋 - 左旋操作
  • \(x\)\(y\) 的右儿子,\(y\)\(z\) 的左儿子,则进行一次左旋 - 右旋操作

实现

void Splay::splay(int x)
{
	for(int i = fa[x];i = fa[x],i; rotate(x))
		if( fa[i] )
		{
			if( get(x) == get(i) )
				rotate(i);
			else
				rotate(x);
		}
	
	root = x;
	
	return ;
}

插入

  1. 如果树为空,则直接插入根节点

  2. 如果找到了一个节点权值与插入权值相等,则增大该节点并维护信息,再进行 Splay 操作

  3. 否则接着往下找,要是找到空节点就直接插入

实现

void Splay::insert(int v)
{
	if( root == 0 )
	{
		tot ++;
		val[tot] = v;
		cnt[tot] ++;
		root = tot;
		maintain(root);
		
		return ;
	}
	
	int cur = root,x = 0;
	while(1)
	{
		if( val[cur] == v )
		{
			cnt[cur] ++;
			maintain(cur);
			maintain(x);
			splay(cur);
			
			break;
		}
		x = cur;
		cur = ch[cur][val[cur] < v];
		
		if( cur == 0 )
		{
			tot ++;
			val[tot] = v;
			cnt[tot] ++;
			fa[tot] = x;
			ch[x][val[x] < v] = tot;
			
			maintain(tot);
			maintain(x);
			splay(tot);
			
			break;
		}
	}
}

寻找数 \(x\) 的排名(比它小的数的个数值 + 1)

  1. \(x\) 小于当前节点权值,则向左子树查找

  2. \(x\) 大于当前节点权值,则答案加上左子树大小 size[i] 和当前节点权值出现次数 cnt[i]

  3. 若找到与 \(x\) 相等的节点,则返回当前答案 \(+ 1\)

实现

int Splay::find_rank(int v)
{
	int ans = 0,cur = root;
	while(1)
	{
		if( v < val[cur] )
			cur = ch[cur][0];
		else
		{
			ans += size[ch[cur][0]];
			if( v == val[cur] )
			{
				splay(cur);
				return ans + 1;
			}
			ans += cnt[cur];
			cur = ch[cur][1];
		}
	}
}

寻找排名为 \(x\) 的数的值

\(v\) 表示剩余排名,在初始排名的条件下不断减少。

  1. 若左子树不为空且剩余排名 \(v\) 小于等于左子树大小 \(size\)(即 \(x\) 在左子树),向左子树查找

  2. 否则减去左子树大小和根的出现次数作为剩余排名 \(v\)。若 \(v\leq 0\),则返回根节点,否则向右子树查找。

实现

int Splay::find_num(int v)
{
	int cur = root;
	while(1)
	{
		if( ch[cur][0] != 0 && v <= size[ch[cur][0]] )
			cur = ch[cur][0];
		else
		{
			v -= cnt[cur] + size[ch[cur][0]];//.//
			if( v <= 0 )
			{
				splay(cur);
				return val[cur];
			}
			cur = ch[cur][1];
		}
	}
}

查询前驱(小于 \(x\) 的最大的数)

先插入节点 \(x\),这样 \(x\) 就处在了根节点的位置。

此时 \(x\) 的左子树都小于 \(x\),寻找 \(x\) 的左子树的最右边节点即小于 \(x\) 的最大的数。

实现

int Splay::pre()
{
	int cur = ch[root][0];
	if( cur == 0 )
		return cur;
	while( ch[cur][1] )
		cur = ch[cur][1];
	
	splay(cur);
	return cur;
}

查询后继(大于 \(x\) 的最小的数)

基本思想与查询前驱相同。

先插入节点 \(x\),这样 \(x\) 就处在了根节点的位置。

此时 \(x\) 的右子树都大于 \(x\),寻找 \(x\) 的右子树的最左边节点即大于 \(x\) 的最小的数。

实现

int Splay::next()
{
	int cur = ch[root][1];
	if( cur == 0 )
		return cur;
	while( ch[cur][0] )
		cur = ch[cur][0];
	
	splay(cur);
	return cur;
}

合并

对于合并两棵树,其中一棵树的值都小于另一棵树的值。

我们可以找到较小一棵树的最大值 \(x\),将其旋转到根节点。

再把较大一棵树作为 \(x\) 的右子树插入。

删除

  1. 首先将 \(x\) 转移到根节点

  2. \(x\) 值不只一个,即 \(cnt[x] > 1\),则直接减一退出即可。

  3. 否则将它的左右两棵子树合并

实现

void Splay::del(int v)
{
	find_rank(v);/////
	
	if( cnt[root] > 1 )
	{
		cnt[root] --;
		maintain(root);
		
		return ;
	}
	
	if( ch[root][0] == 0 && ch[root][1] == 0 )
	{
		clear(root);
		root = 0;
		return ;
	}
	
	if( ch[root][0] == 0 )
	{
		int cur = root;
		root = ch[root][1];
		fa[root] = 0;
		clear(cur);
		
		return ;
	}
	
	if( ch[root][1] == 0 )
	{
		int cur = root;
		root = ch[root][0];
		fa[root] = 0;
		clear(cur);
		
		return ;
	}
	
	int cur = root;
	int x = pre();
	fa[ch[cur][1]] = x;
	ch[x][1] = ch[cur][1];
	clear(cur);
	
	maintain(root);
	
	return ;
}

模板题

Luogu P3369 【模板】普通平衡树

完整代码
#include<bits/stdc++.h>

using namespace std;

const int MAXN = 114514; 

int n;

int root;
// 根节点  
int ch[MAXN][2],fa[MAXN];
//  子节点( 0 左 1 右 )  父节点 
int val[MAXN];
// 权值  
int size[MAXN];
// 子树大小 
int cnt[MAXN];
// 这个权值出现的次数  
int tot;
// 节点个数  

struct Splay{
	void maintain(int x);
	// 维护子树大小 
	bool get(int x);
	// 查找这个节点是父亲的左子树还是右子树  
	void clear(int x);
	// 销毁这个节点 
	void rotate(int x);
	// 旋转  
	void splay(int x);
	// 伸展操作  
	void insert(int v);
	// 插入数 v  
	int find_rank(int v);
	// 查询数 v 的排名  
	int find_num(int v);
	// 查询排名为 v 的数 
	int pre();
	// 查询根节点的前驱  
	int next();
	// 查询根节点的后继  
	void del(int v);
	// 删除 v  
}tree;

void Splay::maintain(int x)
{
	size[x] = size[ch[x][0]] + size[ch[x][1]] + cnt[x];
	return ;
}

bool Splay::get(int x)
{
	if( x == ch[fa[x]][1] )
		return 1;
	return 0;
}

void Splay::clear(int x)
{
	ch[x][0] = 0;
	ch[x][1] = 0;
	fa[x] = 0;
	val[x] = 0;
	size[x] = 0;
	cnt[x] = 0;
	
	return ;
}

void Splay::rotate(int x)
{
	int y = fa[x],z = fa[y],chk = get(x);
	
	ch[y][chk] = ch[x][chk ^ 1];
	
	if( ch[x][chk ^ 1] )
		fa[ch[x][chk ^ 1]] = y;
	ch[x][chk ^ 1] = y;
	
	fa[y] = x;
	fa[x] = z;
	
	if(z)
		ch[z][y == ch[z][1]] = x;
	
	maintain(y);
	maintain(x);
	
	return ;
}

void Splay::splay(int x)
{
	for(int i = fa[x];i = fa[x],i; rotate(x))
		if( fa[i] )
		{
			if( get(x) == get(i) )
				rotate(i);
			else
				rotate(x);
		}
	
	root = x;
	
	return ;
}

void Splay::insert(int v)
{
	if( root == 0 )
	{
		tot ++;
		val[tot] = v;
		cnt[tot] ++;
		root = tot;
		maintain(root);
		
		return ;
	}
	
	int cur = root,x = 0;
	while(1)
	{
		if( val[cur] == v )
		{
			cnt[cur] ++;
			maintain(cur);
			maintain(x);
			splay(cur);
			
			break;
		}
		x = cur;
		cur = ch[cur][val[cur] < v];
		
		if( cur == 0 )
		{
			tot ++;
			val[tot] = v;
			cnt[tot] ++;
			fa[tot] = x;
			ch[x][val[x] < v] = tot;
			
			maintain(tot);
			maintain(x);
			splay(tot);
			
			break;
		}
	}
}

int Splay::find_rank(int v)
{
	int ans = 0,cur = root;
	while(1)
	{
		if( v < val[cur] )
			cur = ch[cur][0];
		else
		{
			ans += size[ch[cur][0]];
			if( v == val[cur] )
			{
				splay(cur);
				return ans + 1;
			}
			ans += cnt[cur];
			cur = ch[cur][1];
		}
	}
}

int Splay::find_num(int v)
{
	int cur = root;
	while(1)
	{
		if( ch[cur][0] != 0 && v <= size[ch[cur][0]] )
			cur = ch[cur][0];
		else
		{
			v -= cnt[cur] + size[ch[cur][0]];//.//
			if( v <= 0 )
			{
				splay(cur);
				return val[cur];
			}
			cur = ch[cur][1];
		}
	}
}

int Splay::pre()
{
	int cur = ch[root][0];
	if( cur == 0 )
		return cur;
	while( ch[cur][1] )
		cur = ch[cur][1];
	
	splay(cur);
	return cur;
}

int Splay::next()
{
	int cur = ch[root][1];
	if( cur == 0 )
		return cur;
	while( ch[cur][0] )
		cur = ch[cur][0];
	
	splay(cur);
	return cur;
}

void Splay::del(int v)
{
	find_rank(v);/////
	
	if( cnt[root] > 1 )
	{
		cnt[root] --;
		maintain(root);
		
		return ;
	}
	
	if( ch[root][0] == 0 && ch[root][1] == 0 )
	{
		clear(root);
		root = 0;
		return ;
	}
	
	if( ch[root][0] == 0 )
	{
		int cur = root;
		root = ch[root][1];
		fa[root] = 0;
		clear(cur);
		
		return ;
	}
	
	if( ch[root][1] == 0 )
	{
		int cur = root;
		root = ch[root][0];
		fa[root] = 0;
		clear(cur);
		
		return ;
	}
	
	int cur = root;
	int x = pre();
	fa[ch[cur][1]] = x;
	ch[x][1] = ch[cur][1];
	clear(cur);
	
	maintain(root);
	
	return ;
}

int main()
{
	scanf("%d",&n);
	for(int i = 1,opt,x;i <= n; i++)
	{
		scanf("%d%d",&opt,&x);
		if( opt == 1 )
			tree.insert(x);
		else if( opt == 2 )
			tree.del(x);
		else if( opt == 3 )///
			printf("%d\n",tree.find_rank(x));
		else if( opt == 4 )////
			printf("%d\n",tree.find_num(x));
		else if( opt == 5 )
		{
			tree.insert(x);
			printf("%d\n",val[tree.pre()]);
			tree.del(x);
		}
		else
		{
			tree.insert(x);
			printf("%d\n",val[tree.next()]);
			tree.del(x);
		}
	}
	return 0;
}

有关伸展树(Splay)详解的更多相关文章

  1. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  2. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  3. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  4. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  5. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  6. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  7. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

  8. VMware虚拟机与本地主机进行磁盘共享(详解) - 2

    VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可

  9. ElasticSearch之 ik分词器详解 - 2

    IK分词器本文分为简介、安装、使用三个角度进行讲解。简介倒排索引众所周知,ES是一个及其强大的搜索引擎,那么它为什么搜索效率极高呢,当然和他的存储方式脱离不了关系,ES采取的是倒排索引,就是反向索引;常见索引结构几乎都是通过key找value,例如Map;倒排索引的优势就是有效利用Value,将多个含有相同Value的值存储至同一位置。分词器为了配合倒排索引,分词器也就诞生了,只有合理的利用Value,才会让倒排索引更加高效,如果一整个Value不进行任何操作直接进行存储,那么Value和key毫无区别。分词器Analyzer通常会对Value进行操作:一、字符过滤,过滤掉html标签;二、分

  10. Educational Codeforces Round 146 (Rated for Div. 2)(B,E详解) - 2

    题外话:抑郁场,开局一小时只出A,死活想不来B,最后因为D题出锅ura才保住可怜的分。但咱本来就写不到DB-LongLegs(数论)本题题解法一学自同样抑郁的知乎作者幽血魅影的题解,有讲解原理。法二来着知乎巨佬cup-pyy(大佬说《不难发现》呜呜)题意三种操作:向上走mmm步向右走mmm步给自己一次走的步数加111,即使得m=m+1m=m+1m=m+1问从(0,0)(0,0)(0,0)走到(a,b)(a,b)(a,b)的最小操作次数,值得注意的是操作三不可逆。解析假设我们最终一步的大小增长到mmm,那么在这个过程中我能以[1,m][1,m][1,m](当步数增长到该数时)之间的任何数字向上或

随机推荐