【数据结构】Splay详解

Splay

      • 引入
  • Splay
      • 旋转操作
      • splay操作
      • 插入操作
      • 查询x排名
      • 查询排名为x
      • 删除操作
      • 查询前驱/后继
      • 模板
      • Splay时间复杂度分析
    • 进阶操作
      • 截取区间
      • 区间加,区间赋值,区间查询,区间最值
      • 区间翻转
      • 原序列整体插入
      • 指定位置插入
      • 整体插入末尾
      • 区间最大子段和
    • 一些好题
    • 参考文献

引入

首先我们要知道一个东西叫二叉搜索树。
其定义如下:

  1. 空树是二叉搜索树。
  2. 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
  3. 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
  4. 二叉搜索树的左右子树均为二叉搜索树。

二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 n n n 个结点的二叉搜索树中,这些操作的最优时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

如图就是一颗典型的 BST(二叉查找树)
在这里插入图片描述
可是我们发现,如果树退化成一条链,那么时间复杂度将退化为 O ( n ) O(n) O(n),这是我们不能接受的,于是平衡树孕育而生,其核心就是维护一颗相对平衡的 BST。
本文将介绍Splay,虽然它并不能保证树一直是"平衡"的,但对于Splay的一系列操作,我们可以证明其每步操作的平摊复杂度都是 O ( log ⁡ n ) O(\log n) O(logn)。所以从某种意义上说,Splay也是一种平衡的二叉查找树。

Splay

旋转操作

下面参考 OI-WIKI的介绍。
在这里插入图片描述
注意,左右旋指的是向左或右旋转。
左旋为ZAG,右旋为ZIG
以下是一次标准旋转操作:
在这里插入图片描述
我们可以知道,旋转流程如下:
在这里插入图片描述

于是我们便可以写出 ZIG和ZAG函数,参考下列代码:
在这里插入图片描述
在这里插入图片描述
不过有时候为了方便表示,我们可以把两个旋转操作合并起来。
就成了 rotate(旋转)函数,以下是参考代码:

void rotate(int x){int y=fa[x],z=fa[y],id=son_(x);ch[y][id]=ch[x][id^1];if(ch[x][id^1])fa[ch[x][id^1]]=y;ch[x][id^1]=y;fa[y]=x;fa[x]=z;if(z)ch[z][y==ch[z][1]]=x;pushup(y);pushup(x);}

其中 son_( x x x)是判断 x x x 为父节点的左儿子还是右儿子,pushup为由下往上更新。

splay操作

这个操作可以说是Splay的核心操作之一,可以理解为把某个点通过旋转操作旋转到根节点。
那么如何将一个节点旋转到根节点呢?
首先有 6 6 6 种基本情况,见下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
那么我们只需要不断重复执行旋转操作,即可旋转到根节点。
以下是参考代码:

void splay(int x) {for (int f = fa[x]; f = fa[x], f; rotate(x))if (fa[f]) rotate(get(x) == get(f) ? f : x);rt = x;
}

一些进阶
由于后面某些操作需要用到,所以我们对splay函数进行一些修改。
具体而言,我们引入一个参数 y y y,让splay把 x x x 旋转到 y y y 的儿子上。(当 y = 0 y=0 y=0 时将 x x x 旋转到根节点)
其实也没什么改动,见参考代码:

void splay(int x,int y){while(fa[x]!=y){if(fa[fa[x]]!=y){if(son_(fa[x])==son_(x))rotate(fa[x]);elserotate(x);}rotate(x);}if(!y)rt=x;
}

插入操作

在这里插入图片描述

解释一下:
二叉树的性质使得插入操作变得非常简易,具体而言,只要值比当前节点大,就往右子树找,小就往左子树找,一样就让计数器+1,如果找不到匹配的值就直接新建一个节点。
参考代码:

	void add(int k){if(!rt){rt=++idx;cnt[rt]++,val[rt]=k;pushup(rt);return ;}int x=rt,y=0;while(1){if(val[x]==k){cnt[x]++;pushup(x),pushup(y);splay(x,0);break;}y=x;x=ch[x][val[x]<k];if(!x){cnt[++idx]++,val[idx]=k;fa[idx]=y;ch[y][val[y]<k]=idx;pushup(idx);pushup(y);splay(idx,0);break;}}}

查询x排名

这个跟插入差不多,从根节点不断往下找,每次向右子树找时加上左子树的size+1,因为左子树和根的值一定比查询值小(BST的性质)。
具体详见代码:

	int x_rank(int k){int rk=0,x=rt;while(1){if(k<val[x])x=ch[x][0];else{rk+=sz[ch[x][0]];if(!x)return rk+1;if(k==val[x]){splay(x,0);return rk+1;}rk+=cnt[x];x=ch[x][1];}}}

查询排名为x

这个跟上面两个操作都差不多,不断往下找就行了。
看着代码,画画图也就能理解了。

	int kth(int k){int x=rt;while(1){if(ch[x][0]&&k<=sz[ch[x][0]])x=ch[x][0];else{k-=sz[ch[x][0]];if(k<=cnt[x]){splay(x,0);return val[x];}k-=cnt[x];x=ch[x][1];}}}

删除操作

在这里插入图片描述
这个就感性理解一下。
参考代码:

	void del(int k){x_rank(k);int x=rt,y=0;if(cnt[rt]>1)cnt[rt]--,pushup(rt);else if(!ch[rt][0]&&!ch[rt][1])clean(rt),rt=0;else if(!ch[rt][0]){rt=ch[rt][1];fa[rt]=0;clean(x);}else if(!ch[rt][1]){rt=ch[rt][0];fa[rt]=0;clean(x);}else{pre();fa[ch[x][1]]=rt;ch[rt][1]=ch[x][1];clean(x),pushup(rt);}}

或者还有一种方式,我们把 x x x 的前驱旋转到根节点,再把 x x x 的后继旋转到根节点的右子树上,这样根节点的右子树的左儿子即为目标节点,直接断开联系即为删除。
参考代码:

void del(int x){int l=kth(x-1),r=kth(r+1);splay(l,0),splay(r,l);fa[ch[r][0]]=0,ch[r][0]=0;pushup(r);pushup(l);
}

查询前驱/后继

这个可以先将这个节点插入,此时它在根节点,那么前驱就是它左子树中最右的点,后继就是它右子树中最左的点。
查询完我们在删除这个点即可。
参考代码:

	int pre(){int z=ch[rt][0];while(ch[z][1])z=ch[z][1];splay(z,0);return z;}int nxt(){int z=ch[rt][1];while(ch[z][0])z=ch[z][0];splay(z,0);return z;}

模板

综合上述操作,我们即可A掉洛谷模版题。
P3369 【模板】普通平衡树

题目概述:
在这里插入图片描述
参考代码:

struct Tr_splay{int fa[N],ch[N][2],sz[N],val[N],cnt[N];void pushup(int x){sz[x]=sz[ch[x][0]]+sz[ch[x][1]]+cnt[x];}void clean(int x){fa[x]=sz[x]=cnt[x]=val[x]=ch[x][0]=ch[x][1]=0;}bool son_(int x){return x==ch[fa[x]][1];}void rotate(int x){int y=fa[x],z=fa[y],id=son_(x);ch[y][id]=ch[x][id^1];if(ch[x][id^1])fa[ch[x][id^1]]=y;ch[x][id^1]=y;fa[y]=x;fa[x]=z;if(z)ch[z][y==ch[z][1]]=x;pushup(y);pushup(x);}void splay(int x,int y){while(fa[x]!=y){if(fa[fa[x]]!=y){if(son_(fa[x])==son_(x))rotate(fa[x]);elserotate(x);}rotate(x);}if(!y)rt=x;}int pre(){int z=ch[rt][0];while(ch[z][1])z=ch[z][1];splay(z,0);return z;}int nxt(){int z=ch[rt][1];while(ch[z][0])z=ch[z][0];splay(z,0);return z;}void add(int k){if(!rt){rt=++idx;cnt[rt]++,val[rt]=k;pushup(rt);return ;}int x=rt,y=0;while(1){if(val[x]==k){cnt[x]++;pushup(x),pushup(y);splay(x,0);break;}y=x;x=ch[x][val[x]<k];if(!x){cnt[++idx]++,val[idx]=k;fa[idx]=y;ch[y][val[y]<k]=idx;pushup(idx);pushup(y);splay(idx,0);break;}}}int x_rank(int k){int rk=0,x=rt;while(1){if(k<val[x])x=ch[x][0];else{rk+=sz[ch[x][0]];if(!x)return rk+1;if(k==val[x]){splay(x,0);return rk+1;}rk+=cnt[x];x=ch[x][1];}}}int kth(int k){int x=rt;while(1){if(ch[x][0]&&k<=sz[ch[x][0]])x=ch[x][0];else{k-=sz[ch[x][0]];if(k<=cnt[x]){splay(x,0);return val[x];}k-=cnt[x];x=ch[x][1];}}}void del(int k){x_rank(k);int x=rt,y=0;if(cnt[rt]>1)cnt[rt]--,pushup(rt);else if(!ch[rt][0]&&!ch[rt][1])clean(rt),rt=0;else if(!ch[rt][0]){rt=ch[rt][1];fa[rt]=0;clean(x);}else if(!ch[rt][1]){rt=ch[rt][0];fa[rt]=0;clean(x);}else{pre();fa[ch[x][1]]=rt;ch[rt][1]=ch[x][1];clean(x),pushup(rt);}}
}tree;
signed main(){IOS;cin>>m;while(m--){int x,y;cin>>x>>y;if(x==1)tree.add(y);if(x==2)tree.del(y);if(x==3)tree.add(y),cout<<tree.x_rank(y)<<"\n",tree.del(y);if(x==4)cout<<tree.kth(y)<<"\n";if(x==5)tree.add(y),cout<<tree.val[tree.pre()]<<"\n",tree.del(y);if(x==6)tree.add(y),cout<<tree.val[tree.nxt()]<<"\n",tree.del(y);}return 0;
}

Splay时间复杂度分析

这个蒟蒻不会,但可以参考 OI-WIKI的证明:
证明

进阶操作

截取区间

Splay还可应用到序列操作中,具体而言,如果我们需要对区间 [ l , r ] [l,r] [l,r]进行操作,我们只需要先将 l − 1 l-1 l1 弄到根节点,再把 r + 1 r+1 r+1 弄到根节点的右儿子上,那么它的左子树就是区间 [ l , r ] [l,r] [l,r]了。
参考代码:

	int split(int l,int r){l=kth(l-1),r=kth(r+1);splay(l,0);splay(r,l);return ch[r][0];}//返回区间[l,r]对应的子树的根节点

区间加,区间赋值,区间查询,区间最值

这个类似线段树,我们相应的维护标记,并写好pushdown即可。
区间加参考:

void pushadd(int x,int k){val[x]+=k;sum[x]+=k*sz[x];add[x]+=k;
}
void modify1(int l,int r,int k){int _=split(l,r);pushadd(_,0,k);pushup(r);pushup(l);
}

区间赋值参考:

void pushcov(int x,int k){val[x]=k;sum[x]=sz[x]*k;add[x]=0;cov[x]=1;
}
void modify(int l,int r,int k){int _=split(l,r);pushcov(_,k);pushup(r);pushup(l);
}

区间查询参考:

void ask_sum(int l,int r){int _=split(l,r);cout<<sum[_]<<"\n";
}

区间翻转

这个呢我们还是搞一个懒标记然后下传,注意各个标记之间的先后顺序。
参考代码:

	void change(int x){swap(ch[x][0],ch[x][1]);lazy[x]^=1;}void reverse(int l,int r){l=kth(l),r=kth(r+2);splay(l,0);splay(r,l);change(ch[ch[l][1]][0]);}

原序列整体插入

有时候题目会直接给我们一个初始序列,一个个插入过于麻烦,于是我们可以类似线段树直接建树。
参考代码:

	int create(int k){int x=top?rb[top--]:++ID;ch[x][0]=ch[x][1]=fa[x]=rev[x]=cov[x]=0;sz[x]=1;val[x]=mx[x]=sum[x]=k;lx[x]=rx[x]=max(0ll,k);return x;}一些毒瘤题卡空间,这样回收可以节省空间。int build(int l,int r,int *a){if(l>r)return 0;if(l==r)return create(a[l]);int mid=(l+r)>>1,x=create(a[mid]);ch[x][0]=build(l,mid-1,a);ch[x][1]=build(mid+1,r,a);fa[ch[x][0]]=fa[ch[x][1]]=x;pushup(x);return x;}
rt=build(1,n,a);

指定位置插入

这个可以参考查询排名为x的操作。
能看到这里说明你已经是大佬了,看着代码画画图即可理解吧。

	void add(int pos,int k){kth(pos);pushdown(rt);fa[ch[rt][0]]=++ID,ch[ID][0]=ch[rt][0];ch[rt][0]=ID,fa[ID]=rt;sz[ID]=1;val[ID]=sum[ID]=k;pushup(ID);pushup(rt);}

整体插入末尾

这个也比较抽象,类似于建一棵新的splay,然后合并。

	void insert(int pos,int len,int *a){int _=build(1,len,a);int y=kth(pos),x=kth(pos+1);splay(y,0);splay(x,y);ch[x][0]=_,fa[_]=x;pushup(x);pushup(y);}

区间最大子段和

参考线段树,我们维护3个标记:
lx:从左起的最大子段和
mx:整个区间的最大子段和
rx:从右起的最大子段和
参考代码:(由于同时维护区间赋值和区间翻转,代码比较抽象)

	void pushup(int x){sz[x]=sz[ch[x][0]]+sz[ch[x][1]]+1;sum[x]=sum[ch[x][0]]+sum[ch[x][1]]+val[x];lx[x]=max(lx[ch[x][0]],sum[ch[x][0]]+val[x]+lx[ch[x][1]]);rx[x]=max(rx[ch[x][1]],sum[ch[x][1]]+val[x]+rx[ch[x][0]]);mx[x]=max(max(mx[ch[x][0]],mx[ch[x][1]]),rx[ch[x][0]]+val[x]+lx[ch[x][1]]);}void pushdown(int x){if(cov[x]){if(ch[x][0])val[ch[x][0]]=val[x],cov[ch[x][0]]=1,sum[ch[x][0]]=val[x]*sz[ch[x][0]];if(ch[x][1])val[ch[x][1]]=val[x],cov[ch[x][1]]=1,sum[ch[x][1]]=val[x]*sz[ch[x][1]];if(val[x]>0){if(ch[x][0])lx[ch[x][0]]=rx[ch[x][0]]=mx[ch[x][0]]=sum[ch[x][0]];if(ch[x][1])lx[ch[x][1]]=rx[ch[x][1]]=mx[ch[x][1]]=sum[ch[x][1]];}else{if(ch[x][0])lx[ch[x][0]]=rx[ch[x][0]]=0,mx[ch[x][0]]=val[x];if(ch[x][1])lx[ch[x][1]]=rx[ch[x][1]]=0,mx[ch[x][1]]=val[x];}cov[x]=0;}if(rev[x]){if(ch[x][0])rev[ch[x][0]]^=1,swap(ch[ch[x][0]][0],ch[ch[x][0]][1]),swap(lx[ch[x][0]],rx[ch[x][0]]);if(ch[x][1])rev[ch[x][1]]^=1,swap(ch[ch[x][1]][0],ch[ch[x][1]][1]),swap(lx[ch[x][1]],rx[ch[x][1]]);rev[x]=0;}}void ask_max_sum(){cout<<mx[rt]<<"\n";}

一些好题

P2042
P4008
P6707

参考文献

  1. OI-WIKI
  2. 伸展树的基本操作和应用——杨思雨
  3. 各位大佬的博客和题解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/46546.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++客户端Qt开发——常用控件(按钮类控件)

2.按钮类控件 ①QPushButton 按钮 继承自QAbstractButton&#xff0c;这个类是⼀个抽象类&#xff0c;是其他按钮的父类 属性 说明 text 按钮中的文本 icon 按钮中的图标 iconSize 按钮中图标的尺寸 shortCut 按钮对应的快捷键 autoRepeat 按钮是否会触发&#xff…

AMD software 将两个显示器合并为一个超宽显示器

最近玩游戏的时候&#xff0c;发现了一个骚操作。 可以将两个显示器&#xff08;更多个的自己去试&#xff0c;不知道&#xff09;组合为一个显示器&#xff0c;注意&#xff0c;这里说的不是将两个显示都连接电脑从而使用双屏显示器&#xff0c; 而是 将两个显示器组合为一个…

基于R语言的水文、水环境模型优化技术及快速率定方法与多模型案例

在水利、环境、生态、机械以及航天等领域中&#xff0c;数学模型已经成为一种常用的技术手段。同时&#xff0c;为了提高模型的性能&#xff0c;减小模型误用带来的风险&#xff1b;模型的优化技术也被广泛用于模型的使用过程。模型参数的快速优化技术不但涉及到优化本身而且涉…

微信小游戏 彩色试管 倒水游戏 逻辑 (二)

最近开始研究微信小游戏&#xff0c;有兴趣的 可以关注一下 公众号&#xff0c; 记录一些心路历程和源代码。 定义一个 Water class 1. **定义接口和枚举**&#xff1a; - WaterInfo 接口定义了水的颜色、高度等信息。 - PourAction 枚举定义了水的倒动状态&#xff0c;…

C双指针元素去重

需求 在尾部插⼊、删除元素是⽐较⾼效的&#xff0c;时间复杂度 是 O(1)&#xff0c;但是如果在中间或者开头插⼊、删除元素&#xff0c;就会涉及数据的搬移&#xff0c;时间复杂度为 O(N)&#xff0c;效率较低。 代码 #include <stdio.h>// 相邻元素去重 int remove…

01 电场强度通量 高斯定理

电场强度通量 高斯定理 5-4 电场强度通量 高斯定理一.电场线二.电场强度通量三.高斯定理四高斯定理应用举例典型电场的电场线分布图形正点电荷与负点电荷的电场线一对等量正点电荷的电场线一对等量异号点电荷的电场线一对不等量异号点电荷的电场线带电平行板电容器的电场线 5-4…

CompletableFuture介绍与实战

CompletableFuture 介绍与实战 一、前言 ​ 日常工作中&#xff0c;大多数情况下我们的接口的执行逻辑都是串行化的&#xff0c;串行化的逻辑也基本能满足我们绝大部分的场景。但是&#xff0c;在一些情况下我们的代码可能会存在一些比较耗时的操作&#xff0c;串行的逻辑就有…

金蝶云星空与金蝶云星空对接集成付款单查询打通[标准][付款单新增]-v1

金蝶云星空与金蝶云星空对接集成付款单查询打通[标准][付款单新增]-v1 对接源平台:金蝶云星空 金蝶K/3Cloud在总结百万家客户管理最佳实践的基础上&#xff0c;提供了标准的管理模式&#xff1b;通过标准的业务架构&#xff1a;多会计准则、多币别、多地点、多组织、多税制应用…

Gstreamer学习3.1------使用appsrc灌颜色信号数据

这个视频内容讲解的离散余弦变换&#xff0c;讲的很好&#xff0c; 离散余弦变换可视化讲解_哔哩哔哩_bilibili 其中讲到&#xff0c;把颜色变化转换为曲线的处理&#xff0c; 在前面的学习中&#xff0c;我们知道了可以向appsrc来灌数据来进行显示 Gstreamer学习3----灌数据…

yolo格式数据集之野生动物类4种数据集已划分好|可以直接使用|yolov5|v6|v7|v8|v9|v10通用

本数据为野生动物类检测数据集&#xff0c;数据集数量如下&#xff1a; 总共有:1504张 训练集&#xff1a;1203张 验证集&#xff1a;150张 类别数量&#xff1a;4 测试集&#xff1a;151 类别名&#xff1a; [‘buffalo’, ‘elephant’, ‘rhino’, ‘zebra’] 占用空间&…

自动驾驶-端到端分割任务

上采样 bed of nails interpolation transposed convolutions 1. 上采样 (Upsampling) 上采样是一种技术&#xff0c;用于增加数据集中的样本数量或是提高信号的分辨率。在图像处理中&#xff0c;上采样通常指的是增加图像的像素数量&#xff0c;从而使图像变得更大。这可…

如何用STM32实现modbus-RTU?

Modbus RTU是一种广泛应用于工业自动化领域的通信协议,基于主从架构,通过串行通信进行数据传输。本文将详细介绍Modbus RTU协议的基本原理,并提供在STM32微控制器上实现Modbus RTU通信的完整代码示例。 1. Modbus RTU协议概述 Modbus RTU的定义和特点 Modbus RTU(Remote Te…

哥德巴赫猜想c++

方法一 #include<bits/stdc.h> using namespace std; //定义函数&#xff0c;判断素数 bool sushu(int n){bool rtrue;//先假设是素数&#xff0c;即真//循环因子范围&#xff0c;找到一个因子就不是素数for(int i2;i<sqrt(n);i){//判断2~n的根号是否素数if(n%i0){//…

【Linux】从零开始认识多线程 --- 线程控制

在这个浮躁的时代 只有自律的人才能脱颖而出 -- 《觉醒年代》 从零开始认识多线程 --- 线程控制 1 知识回顾2 线程控制2.1 线程创建2.2 线程等待2.3 线程终止 3 测试运行3.1 小试牛刀 --- 创建线程3.2 探幽析微 --- 理解线程参数3.3 小有心得 --- 探索线程返回3.4 求索无厌 …

数据结构_顺序表专题

何为数据结构&#xff1f; 咱今天也来说道说道...... 数据结构介绍 准确概念 数据结构就是计算机存储、组织数据的方式 概念分析 从上句分析&#xff0c;数据结构是一种方式。一种管理数据的方式。为了做什么&#xff1f;为的就是计算机存储数据&#xff0c;组织数据。 …

元器件基础学习笔记——磁珠

一、磁珠的作用及构造 1.1 磁珠的作用 磁珠是一种用于抑制高频噪声的被动电子组件&#xff0c;通常由铁氧体材料制成&#xff0c;这种材料具有高电阻率和高磁导率&#xff0c;使其能够在高频下有效地将干扰信号以热能的形式消耗掉。在电路设计中&#xff0c;磁珠被广泛用于信号…

LeetCode 441, 57, 79

目录 441. 排列硬币题目链接标签思路代码 57. 插入区间题目链接标签思路两个区间的情况对每个区间的处理最终的处理 代码 79. 单词搜索题目链接标签原理思路代码 优化思路代码 441. 排列硬币 题目链接 441. 排列硬币 标签 数学 二分查找 思路 由于本题所返回的 答案在区间…

3d复制的模型怎么渲染不出来?----模大狮模型网

在展览3D模型设计领域&#xff0c;技术的进步和创新使得模型的复杂性和精细度有了显著提升。然而&#xff0c;有时设计师们在尝试渲染复杂的3D复制模型时&#xff0c;却面临着无法正确呈现的问题。模大狮将探讨这一现象的可能原因&#xff0c;并提供相应的解决方案和建议&#…

知识图谱和 LLM:利用Neo4j驾驭大型语言模型(探索真实用例)

这是关于 Neo4j 的 NaLLM 项目的一篇博客文章。这个项目是为了探索、开发和展示这些 LLM 与 Neo4j 结合的实际用途。 2023 年,ChatGPT 等大型语言模型 (LLM) 因其理解和生成类似人类的文本的能力而风靡全球。它们能够适应不同的对话环境、回答各种主题的问题,甚至模拟创意写…

3d导入模型后墙体变成黑色?---模大狮模型网

在展览3D模型设计领域&#xff0c;技术和设计的融合通常是创意和实现之间的桥梁。然而&#xff0c;有时设计师们会遇到一些技术上的挑战&#xff0c;如导入3D模型后&#xff0c;墙体却突然变成了黑色。这种问题不仅影响了设计的视觉效果&#xff0c;也反映了技术应用中的一些复…