\(\texttt{0x01}\) 前言

Splay 树(伸展树)是一棵二叉搜索树,由 Daniel SleatorRobert Tarjan 于 1985 年发明。它凭借旋转可以有 $O(\log n) $ 插入,删除等的较优秀的时间复杂度。

前置芝士:普通二叉排序树

推荐博客:

\(\texttt{0x02}\) 如何构造一棵 Splay

我们定义一个结构体:

#define val(x) t[x].val
#define ls(x) t[x].ch[0]
#define rs(x) t[x].ch[1]
#define son(x,nxt) t[x].ch[nxt]
#define fa(x) t[x].fa
#define cnt(x) t[x].cnt
#define siz(x) t[x].siz
struct node{
int val,fa,ch[2],siz,cnt;
}t[N];
int root,tot;

其中构造一个新节点的函数长这样:

void newPoint(int val,int fa,int nxt){ //值为val,父节点为fa,为fa的nxt儿子
tot++;
fa(tot)=fa; cnt(tot)=siz(tot)=1; val(tot)=val;
son(fa,nxt)=tot;
}

\(\texttt{0x03}\) which / pushup / connect

which 的作用是判断 \(x\) 是其父节点的左节点(\(0\))还是右节点(\(1\)),代码很好写:

bool which(int x){
return rs(fa(x))==x;
}

pushup 的作用是维护当前节点的 \(siz\) 信息,和线段树的 pushup 性质差不多,代码:

void pushup(int x){
siz(x)=siz(ls(x))+siz(rs(x))+cnt(x);//记得加上当前节点的cnt
}

connect 的作用是把 \(x\) 变成 \(y\) 的 \(nxt\) 儿子,无需考虑覆盖的问题,代码也很简洁:

void connect(int x,int y,int nxt){
son(y,nxt)=x;
fa(x)=y;
}

\(\texttt{0x04}\) rotate

Splay 的核心操作:旋转。

放两张动图:



我们会发现:右旋时,E 节点要到 S 节点的位置上,那么 E 节点的右儿子因为它 \(\ge E\) 且 \(\le S\),所以只能放在 S 节点的左儿子,然后要改变 E 和 S 的父子关系。最后别忘了因为有旋转,所以要自下而上更新节点信息。

左旋同理。

代码:

void rotate(int x){
int y=fa(x),z=fa(y);
int fx=which(x),fy=which(y); connect(son(x,fx^1),y,fx); //如果x是左儿子,改变它右儿子的位置,反之同理
connect(y,x,fx^1); //把y接到x的缺失的那一棵子树上
connect(x,z,fy); //把x接到y的父节点上去 pushup(y); pushup(x); //别搞错顺序
}

\(\texttt{0x05}\) splay

Splay 树保证时间复杂度正确的核心操作,把 \(x\) 转到 \(y\) 的位置(\(y\) 通常为 \(root\))。

有几点结论,难证但好记:

  • 若 \(fa(x)=y\),则单旋 \(x\)。
  • 若 \(x\)、\(fa(x)\)、\(fa\left(fa(x)\right)\) 不在一条线上,则先单旋 \(fa(x)\),再单旋 \(x\)。
  • 否则旋转两次 \(x\)。
void splay(int x,int y){
y=fa(y); //避免x=y时出现的错误
while(fa(x)!=y){
if(fa(fa(x))==y) // Case 1
rotate(x);
else if(which(x)==which(fa(x))) // Case 2
rotate(fa(x)), rotate(x);
else // Case 3
rotate(x), rotate(x);
}
if(y==0){ // 如果y是根,把根变为x
root=x;
connect(x,0,1);
}
}

\(\texttt{0x06}\) insert

与普通的二叉排序树基本一致。

  • 如果树中已经有值了,则 \(cnt \gets cnt+1\)。
  • 如果找到最后都没有值,建个新节点。

记得最后要 splay 一下,把这个点转到根节点。

void insert(int val){
if(root==0){
newPoint(val,0,1);
root=tot;
return;
}
int now=root;
while(1){
siz(now)++;
if(val(now)==val){
cnt(now)++;
splay(now,root);
return;
}
int nxt=val(now)<val, son=son(now,nxt);
if(!son){
newPoint(val,now,nxt);
splay(tot,root);
return;
}
now=son;
}
}

\(\texttt{0x07}\) find

这一步操作是找到树中值为 \(val\) 的节点,并把它旋转到根节点,为 delete 操作做准备。

与普通二叉排序树也基本一致。

int find(int val){
int now=root;
while(1){
if(!now)
return 0;
if(val(now)==val){
splay(now,root);
return now;
}
int nxt=val(now)<val, son=son(now,nxt);
now=son;
}
}

\(\texttt{0x08}\) delete

目的是删除树中值为 \(val\) 的节点。

先 find 这个节点,让他转到根,然后分类讨论。

  1. 树中没有值为 \(val\) 的节点,删了个寂寞。

  2. 树中值为 \(val\) 的节点有不止一个(即 \(cnt \ge 2\)),让 \(cnt \gets cnt-1\) 即可。

  3. 这个节点没有左儿子(即根节点只有右子树),把右儿子设为根就行了。

  4. 这个节点没有右儿子(即根节点只有左子树),把左儿子设为根就行了。

  5. 这个节点(设为 \(x\))有左右儿子,把它的左子树中值最大的(设为 \(y\))splay 到根,然后现在的 Splay 树的根就是 \(y\),左子树是原来的除 \(y\) 之外的左子树,右子树是 \(x\) 和之前的右子树。把之前的右子树 connect 到根就行了。

注意:这里的删除操作都没有回收编号

void delet(int val){
int now=find(val);
if(!now) return;
if(cnt(now)>1){
cnt(now)--; siz(now)--;
return;
}
if(!ls(now) && !rs(now)){
root=0;
}
else if(!ls(now)){
root=rs(root);
fa(root)=0;
}
else if(!rs(now)){
root=ls(root);
fa(root)=0;
}
else{
int pos=ls(now);
while(rs(pos)) pos=rs(pos); splay(pos,root);
connect(rs(now),pos,1);
pushup(pos);
}
}

\(\texttt{0x09}\) rnk & find_k

rnk 是返回值为 \(val\) 的数在树中的排名,find_k 是找到树中排名为 k 的数。

与二叉排序树基本完全相同。记得最后要把节点 splay 到根。

int rnk(int val){
int now=root,s=0;
while(now){
if(val(now)==val){
splay(now,root);
return siz(ls(now))+1;
}
if(val(now)<val){
s+=siz(ls(now))+cnt(now);
now=rs(now);
}
else{
now=ls(now);
}
}
return s+1;
}
int find_k(int k){
int now=root;
while(1){
int used=siz(now)-siz(rs(now));
if(k>siz(ls(now)) && k<=used){
break;
}
if(k>=used){
k-=used;
now=rs(now);
}
else{
now=ls(now);
}
}
splay(now,root);
return val(now);
}

\(\texttt{0x0A}\) lower & upper

返回值为 \(val\) 的数的前驱和后继。

int lower(int val){
int ans=-2147483647;
int now=root;
while(now){
if(val(now)<val && val(now)>ans){
ans=val(now);
}
if(val>val(now)){
now=rs(now);
}
else{
now=ls(now);
}
}
return ans;
}
int upper(int val){
int ans=2147483647;
int now=root;
while(now){
if(val(now)>val && val(now)<ans){
ans=val(now);
}
if(val<val(now)){
now=ls(now);
}
else{
now=rs(now);
}
}
return ans;
}

\(\texttt{0x0B}\) 完整代码

#include<bits/stdc++.h>
using namespace std;
constexpr int N = 5e5+5; template <typename T> void read(T &x){x=0; T f(0); char ch=getchar(); while(ch<'0'||ch>'9'){f|=ch=='-';ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48); ch=getchar();} x=f?-x:x;}
template <typename T,typename ...Arg>void read(T& x,Arg& ...arg){read(x);read(arg...);}
template <typename T> inline void write(T x){static char buf[64]; static int tot(0); if(x<0) putchar('-'),x=-x; do buf[++tot]=(x%10)+48,x/=10; while(x); do putchar(buf[tot--]); while(tot);}
template <typename T> void write(T x,char c){static char buf[64]; static int tot(0); if(x<0) putchar('-'),x=-x; do buf[++tot]=(x%10)+48,x/=10; while(x); do putchar(buf[tot--]); while(tot); putchar(c);} class Splay{
#define val(x) t[x].val
#define ls(x) t[x].ch[0]
#define rs(x) t[x].ch[1]
#define son(x,nxt) t[x].ch[nxt]
#define fa(x) t[x].fa
#define cnt(x) t[x].cnt
#define siz(x) t[x].siz
private:
struct node{
int val,fa,ch[2],siz,cnt;
}t[N];
int root,tot;
public:
bool which(int x){
return rs(fa(x))==x;
}
void pushup(int x){
siz(x)=siz(ls(x))+siz(rs(x))+cnt(x);
}
void connect(int x,int y,int nxt){
son(y,nxt)=x;
fa(x)=y;
}
void rotate(int x){
int y=fa(x),z=fa(y);
int fx=which(x),fy=which(y); connect(son(x,fx^1),y,fx);
connect(y,x,fx^1);
connect(x,z,fy); pushup(y); pushup(x);
}
void splay(int x,int y){
y=fa(y);
while(fa(x)!=y){
if(fa(fa(x))==y)
rotate(x);
else if(which(x)==which(fa(x)))
rotate(fa(x)), rotate(x);
else
rotate(x), rotate(x);
}
if(y==0){
root=x;
connect(x,0,1);
}
}
void newPoint(int val,int fa,int nxt){
tot++;
fa(tot)=fa; cnt(tot)=siz(tot)=1; val(tot)=val;
son(fa,nxt)=tot;
}
void insert(int val){
if(root==0){
newPoint(val,0,1);
root=tot;
return;
}
int now=root;
while(1){
siz(now)++;
if(val(now)==val){
cnt(now)++;
splay(now,root);
return;
}
int nxt=val(now)<val, son=son(now,nxt);
if(!son){
newPoint(val,now,nxt);
splay(tot,root);
return;
}
now=son;
}
}
int find(int val){
int now=root;
while(1){
if(!now)
return 0;
if(val(now)==val){
splay(now,root);
return now;
}
int nxt=val(now)<val, son=son(now,nxt);
now=son;
}
}
void delet(int val){
int now=find(val);
if(!now) return;
if(cnt(now)>1){
cnt(now)--; siz(now)--;
return;
}
if(!ls(now) && !rs(now)){
root=0;
}
else if(!ls(now)){
root=rs(root);
fa(root)=0;
}
else if(!rs(now)){
root=ls(root);
fa(root)=0;
}
else{
int pos=ls(now);
while(rs(pos)) pos=rs(pos); splay(pos,root);
connect(rs(now),pos,1);
pushup(pos);
}
}
int rnk(int val){
int now=root,s=0;
while(now){
if(val(now)==val){
splay(now,root);
return siz(ls(now))+1;
}
if(val(now)<val){
s+=siz(ls(now))+cnt(now);
now=rs(now);
}
else{
now=ls(now);
}
}
return s+1;
}
int find_k(int k){
int now=root;
while(1){
int used=siz(now)-siz(rs(now));
if(k>siz(ls(now)) && k<=used){
break;
}
if(k>=used){
k-=used;
now=rs(now);
}
else{
now=ls(now);
}
}
splay(now,root);
return val(now);
}
int lower(int val){
int ans=-2147483647;
int now=root;
while(now){
if(val(now)<val && val(now)>ans){
ans=val(now);
}
if(val>val(now)){
now=rs(now);
}
else{
now=ls(now);
}
}
return ans;
}
int upper(int val){
int ans=2147483647;
int now=root;
while(now){
if(val(now)>val && val(now)<ans){
ans=val(now);
}
if(val<val(now)){
now=ls(now);
}
else{
now=rs(now);
}
}
return ans;
}
}tr; int n,opt,val; int main(){
read(n);
while(n--){
read(opt,val);
int ans;
switch(opt){
case 1:{
tr.insert(val);
break;
}
case 2:{
tr.delet(val);
break;
}
case 3:{
ans=tr.rnk(val);
break;
}
case 4:{
ans=tr.find_k(val);
break;
}
case 5:{
ans=tr.lower(val);
break;
}
case 6:{
ans=tr.upper(val);
break;
}
}
if(opt>2) write(ans,'\n');
}
}

【学习笔记】Splay的更多相关文章

  1. [学习笔记] Splay Tree 从入门到放弃

    前几天由于出行计划没有更博QwQ (其实是因为调试死活调不出来了TAT我好菜啊) 伸展树 伸展树(英语:Splay Tree)是一种二叉查找树,它能在O(log n)内完成插入.查找和删除操作.它是由 ...

  2. [学习笔记]Splay

    其实就是一道题占坑啦 [NOI2005]维护数列 分析: 每次操作都要 \(Splay\) 一下 \(Insert\) 操作:重建一棵平衡树,把 \(l\) 变成根,\(l+2\) 变成右子树的根,那 ...

  3. 平衡树splay学习笔记#2

    讲一下另外的所有操作(指的是普通平衡树中的其他操作) 前一篇的学习笔记连接:[传送门],结尾会带上完整的代码. 操作1,pushup操作 之前学习过线段树,都知道子节点的信息需要更新到父亲节点上. 因 ...

  4. [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家

    1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...

  5. 平衡树学习笔记(3)-------Splay

    Splay 上一篇:平衡树学习笔记(2)-------Treap Splay是一个实用而且灵活性很强的平衡树 效率上也比较客观,但是一定要一次性写对 debug可能不是那么容易 Splay作为平衡树, ...

  6. BST,Splay平衡树学习笔记

    BST,Splay平衡树学习笔记 1.二叉查找树BST BST是一种二叉树形结构,其特点就在于:每一个非叶子结点的值都大于他的左子树中的任意一个值,并都小于他的右子树中的任意一个值. 2.BST的用处 ...

  7. 学习笔记 | CDQ分治

    目录 前言 啥是CDQ啊(它的基本思想) 例题 后记 参考博文 前言 博主太菜了 学习快一年的OI了 好像没有什么会的算法 更寒碜的是 学一样还不精一样TAT 如有什么错误请各位路过的大佬指出啊感谢! ...

  8. OI知识点|NOIP考点|省选考点|教程与学习笔记合集

    点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...

  9. 平衡树学习笔记(6)-------RBT

    RBT 上一篇:平衡树学习笔记(5)-------SBT RBT是...是一棵恐怖的树 有多恐怖? 平衡树中最快的♂ 不到200ms的优势,连权值线段树都无法匹敌 但是,通过大量百度,发现RBT的代码 ...

  10. 平衡树学习笔记(5)-------SBT

    SBT 上一篇:平衡树学习笔记(4)-------替罪羊树 所谓SBT,就是Size Balanced Tree 它的速度很快,完全碾爆Treap,Splay等平衡树,而且代码简洁易懂 尤其是插入节点 ...

随机推荐

  1. vulnhub靶场之ICA: 1

    准备: 攻击机:虚拟机kali.本机win10. 靶机:ICA: 1,网段地址我这里设置的桥接,所以与本机电脑在同一网段,下载地址:https://download.vulnhub.com/ica/i ...

  2. Nginx配置-1

    1.绑定nginx到指定cpu [root@nginx conf.d]# vim /apps/nginx/conf/nginx.conf worker_processes 2; worker_cpu_ ...

  3. 微信支付v3接口的 官方 Java SDK

    啰嗦几句:微信支付v3版接口麻烦吗?在对接微信支付v3接口时,本来是一件很简单的事情,其实微信支付v3接口并不是很复杂,但是微信团队的管理很混乱,给我们开发者带来了巨大的麻烦. 微信支付v3版接口对接 ...

  4. Redisson源码解读-分布式锁

    前言 Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid).Redisson有一样功能是可重入的分布式锁.本文来讨论一下这个功能的特点以及源 ...

  5. .NET 7.0 重磅发布及资源汇总

    2022-11-8 .NET 7.0 作为微软的开源跨平台开发平台正式发布.微软在公告中表示.NET 7为您的应用程序带来了C# 11 / F# 7,.NET MAUI,ASP.NET Core/Bl ...

  6. Springboot结构梳理

    springboot各层关系梳理 1.基本流程 View层-->Controller层(响应用户请求):导入 service层,调用你service方法,controller通过接受前端传来的参 ...

  7. 优秀的Kafka GUI客户端、可视化管理工具、监控工具

    想要查看Topic里的消息却找不到软件,想要查看或更新Broker.Topic配置,想要监控Broker服务器状态?试试下面的Kafka GUI工具--Kafka Assistant 官网地址:htt ...

  8. qt quick工程升级,qmake工程升级至cmake

    升级原因 由于音视频工作需要,qt6比qt5的video相关更看重效率. 升级中遇到的问题 在开发的过程中,为了更快速的进行开发,对业务其他不需要代码运行效率的地方使用qml+js的方式进行编写.在升 ...

  9. mybatis sql批量插入

    insert into jrqf_officialcard (id, budget_unit, money_purpose, economic_type, money, func_subject_na ...

  10. WeetCode3 暴力递归->记忆化搜索->动态规划

    笔者这里总结的是一种套路,这种套路笔者最先是从左程云的b站视频学习到的 本文进行简单总结 系列文章目录和关于我 一丶动态规划的思想 使用dp数组记录之前状态计算的最佳结果,找出当前状态和之前状态的关系 ...