在平衡树的海洋中畅游(三)——Splay

时间:2021-12-06 21:19:57

Preface

由于我怕学习了Splay之后不直接写blog第二天就忘了,所以强行加了一波优先级

论谁是天下最秀平衡树,我Splay第一个不服。维护平衡只靠旋转。

一言不合转死你

由于平衡树我也介绍了两种Treap&&Scapegoat Tree,所以一些互通的东西也不讲了。

这次的亮点主要是为了弥补Splay的巨大常数(据说是96),我把大量的函数都写成了迭代版本。

废话不多说开讲。


维护平衡的方式——旋转

关于旋转操作,在Treap的那篇文章中已经讲的比较详细了。

但是注意一下Splay的旋转不同于Treap,一般Splay旋转时简单的理解就是儿子翻身当爸爸

什么意思,其实像这样:

在平衡树的海洋中畅游(三)——Splay

\(x,y\)的关系还是很好理解的,主要是\(x\)的某个儿子被旋到哪里去了旋的头晕

我们再手玩几种情况:

  • \(y\)是\(z\)的左儿子 \(x\)是\(y\)的左儿子

  • \(y\)是\(z\)的左儿子 \(x\)是\(y\)的右儿子

  • \(y\)是\(z\)的右儿子 \(x\)是\(y\)的右儿子

疯狂手玩ing

那我们来总结一波性质吧:

  1. 我们把\(x\)转到了原来\(y\)的位置(这个很简单,因为我们旋转的本意就是让\(x\)儿子变爸爸)
  2. \(x\)是\(y\)的哪个儿子,那么旋转完之后,\(x\)的那个儿子就不会变(多玩几次就好了,证明也比较简单)
  3. 如果原来\(x\)是\(y\)的哪一个儿子,那么旋转完之后\(y\)就是\(x\)的另外一个儿子

然后我们就可以搞出旋转的核心代码了(具体看下面)


一波操作上天——伸展

为什么Splay要叫Splay呢。请自行使用词典查询Splay的意思。好了我懂了

一个节点已经不满足屈膝于它人的儿子了,我就是想俯视众生

正常的说,就是想要当根节点。真有梦想

那么让你飞,但是我只需要无脑暴力上旋就可以维护平衡这一重要性质了吗?

我将一个\(x\)进行上旋操作,像这样:

在平衡树的海洋中畅游(三)——Splay

不是说好了要当根的吗,那我把\(x\)再旋转一波:

在平衡树的海洋中畅游(三)——Splay

然后我们很意外的发现,红色的那一条链一点都没有改变

也就意味这数据还是可以把你给卡掉

然后我们发现,对于\(x,y,z\)的不同位置情况,我们要分别进行讨论

然后蒟蒻就开始\(2^3=8\)种情况大力讨论

手玩了好久好久。。。。。。

其实简化了之后也就两种:

  1. \(x\)和\(y\)分别是\(y\)和\(z\)的同一个儿子
  2. \(x\)和\(y\)分别是\(y\)和\(z\)的不同儿子

而对于情况一,也就是类似上面给出的图的情况,就要考虑先旋转\(y\)再旋转\(x\)

而对于情况二,自己画一下图,发现就是对\(x\)旋转两次,先旋转到\(y\)再旋转到\(x\)

于是我们就可以简化一波操作,就完成了Splay的核心内容。


其它操作及模板

其它的一些操作都是具有BST性质的了,就是主要一点:

有事没事Splay一下要不然干嘛叫Splay

还是板子题:Luogu P3369 【模板】普通平衡树的CODE(跑的并不慢)

#include<cstdio>
#include<cctype>
#define lc(x) (node[x].ch[0])
#define rc(x) (node[x].ch[1])
#define fa(x) (node[x].fa)
#define tc() (A==B&&(B=(A=fl)+fread(fl,1,S,stdin),A==B)?EOF:*A++)
#define pc(ch) (top<S?st[top++]=ch:(fwrite(st,1,S,stdout),st[(top=0)++]=ch))
using namespace std;
const int N=100005,S=1<<20,INF=2147483647;
char fl[S],st[S],*A=fl,*B=fl;
struct Splay
{
int ch[2],size,cnt,fa,val;
}node[N];//同Treap,注意记录节点重复个数
int n,opt,x,rt,tot,top;
inline void read(int &x)
{
x=0; char ch; int flag=1; while (!isdigit(ch=tc())) flag=ch^'-'?1:-1;
while (x=(x<<3)+(x<<1)+ch-'0',isdigit(ch=tc())); x*=flag;
}
inline void write(int x)
{
if (x<0) pc('-'),x=-x;
if (x>9) write(x/10); pc(x%10+'0');
}
inline void pushup(int now)//更新操作,和一般的BST大同小异
{
node[now].size=node[lc(now)].size+node[rc(now)].size+node[now].cnt;
}
inline int build(int val,int fa)//新建一个节点
{
node[++tot].val=val; node[tot].size=node[tot].cnt=1; fa(tot)=fa;
lc(tot)=rc(tot)=0; if (fa) node[fa].ch[val>node[fa].val]=tot; return tot;
}
inline int identify(int now)//确认一个点是它父亲的左儿子(0)还是右儿子(1)
{
return node[fa(now)].ch[1]==now;
}
inline void connect(int son,int fa,int d)//链接两个节点,d(0/1)表示方向
{
fa(son)=fa; node[fa].ch[d]=son;
}
inline void rotate(int now)//核心操作,上旋。注意connect的次序,最容易写错的就是pushup,不能pushup反了
{
int x=node[now].fa,y=node[x].fa,dx=identify(now),dy=identify(x),son=node[now].ch[dx^1];
connect(son,x,dx); connect(x,now,dx^1); connect(now,y,dy); pushup(x); pushup(now);
}
inline void splay(int now,int tar)//splay,将now伸展成为tar的儿子,注意讨论父节点即为根的情况
{
while (fa(now)!=tar)
{
if (fa(fa(now))!=tar)
{
if (identify(now)^identify(fa(now))) rotate(now); else rotate(fa(now));
} rotate(now);
}
if (!tar) rt=now;
}
inline void get_rank(int val)//查询一个点的排名,并把它旋转到根
{
int now=rt; if (!now) return;
while (node[now].val!=val&&node[now].ch[val>node[now].val])
now=node[now].ch[val>node[now].val]; splay(now,0);
}
inline int get_val(int rk)//得到排名为rk的值
{
int now=rt;
for (;;)
{
if (rk>node[lc(now)].size+node[now].cnt) rk-=node[lc(now)].size+node[now].cnt,now=rc(now);
else if (node[lc(now)].size>=rk) now=lc(now); else return node[now].val;
}
}
inline int next(int val,int d)//精简的求前驱(0),后继(1)的过程,注意特判
{
get_rank(val); int now=rt;
if ((node[now].val>val&&d)||(node[now].val<val&&!d)) return now;
now=node[now].ch[d]; while (node[now].ch[d^1]) now=node[now].ch[d^1]; return now;
}
inline void insert(int val)//插入一个新的节点,由于此时平衡性得不到保证所以又要splay到根
{
int now=rt,fa=0;
while (now&&node[now].val!=val) fa=now,now=node[now].ch[val>node[now].val];
if (!now) now=build(val,fa); else ++node[now].cnt; splay(now,0);
}
inline void remove(int val)//删除就是把一个点的前驱后继都旋上去,此时这个点就成为叶子节点了,再进行删除
{
int lst=next(val,0),nxt=next(val,1); splay(lst,0); splay(nxt,lst); int del=lc(nxt);
if (node[del].cnt>1) --node[del].cnt,splay(del,0); else lc(nxt)=0;
}
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
register int i; read(n); insert(-INF); insert(+INF);
for (i=1;i<=n;++i)
{
read(opt); read(x);
switch (opt)
{
case 1:insert(x);break;
case 2:remove(x);break;
case 3:get_rank(x),write(node[lc(rt)].size),pc('\n');break;
case 4:write(get_val(x+1)),pc('\n');break;
case 5:write(node[next(x,0)].val),pc('\n');break;
case 6:write(node[next(x,1)].val),pc('\n');break;
}
}
return fwrite(st,1,top,stdout),0;
}

平衡树的精髓在于它们不同的维护平衡的方式:

  • Treap的代码精简且常数较小,是解决大部分平衡树问题的最好工具
  • Scapegoat Tree的思想易于理解,且一般情况下速率良好。其内核的思想也应用到了许多其他的地方
  • Splay由于它特殊的旋转性质,使其可以像线段树一样维护区间,也是LCT的辅助树,功能强大。常数也很大

在平衡树的海洋中畅游(三)——Splay的更多相关文章

  1. 在平衡树的海洋中畅游(一)——Treap

    记得有一天翔哥毒奶我们: 当你们已经在平衡树的海洋中畅游时,我还在线段树的泥沼中挣扎. 我觉得其实像我这种对平衡树一无所知的蒟蒻也要开一开数据结构了. 然后花了一天啃了下最简单的平衡树Treap,感觉 ...

  2. 在平衡树的海洋中畅游(四)——FHQ Treap

    Preface 关于那些比较基础的平衡树我想我之前已经介绍的已经挺多了. 但是像Treap,Splay这样的旋转平衡树码亮太大,而像替罪羊树这样的重量平衡树却没有什么实际意义. 然而类似于SBT,AV ...

  3. 在平衡树的海洋中畅游(二)——Scapegoat Tree

    在平衡树的广阔天地中,以Treap,Splay等为代表的通过旋转来维护平衡的文艺平衡树占了觉大部分. 然而,今天我们要讲的Scapegoat Tree(替罪羊树)就是一个特立独行的平衡树,它通过暴力重 ...

  4. &lbrack;BZOJ5020&rsqb;&lbrack;THUWC2017&rsqb;在美妙的数学王国中畅游&lpar;LCT&rpar;

    5020: [THUWC 2017]在美妙的数学王国中畅游 Time Limit: 80 Sec  Memory Limit: 512 MBSec  Special JudgeSubmit: 323  ...

  5. 【BZOJ5020】【THUWC2017】在美妙的数学王国中畅游(Link-Cut Tree,组合数学)

    [BZOJ5020][THUWC2017]在美妙的数学王国中畅游(Link-Cut Tree,组合数学) 题解 Description 数字和数学规律主宰着这个世界. 机器的运转, 生命的消长, 宇宙 ...

  6. &lbrack;THUWC2017&rsqb;在美妙的数学王国中畅游

    [THUWC2017]在美妙的数学王国中畅游 e和sin信息不能直接合并 泰勒展开,大于21次太小,认为是0,保留前21次多项式即可 然后就把e,sin ,kx+b都变成多项式了,pushup合并 上 ...

  7. 【BZOJ5020】&lbrack;THUWC 2017&rsqb;在美妙的数学王国中畅游 泰勒展开&plus;LCT

    [BZOJ5020][THUWC 2017]在美妙的数学王国中畅游 Description 数字和数学规律主宰着这个世界. 机器的运转, 生命的消长, 宇宙的进程, 这些神秘而又美妙的过程无不可以用数 ...

  8. Java三大框架之——Hibernate中的三种数据持久状态和缓存机制

    Hibernate中的三种状态   瞬时状态:刚创建的对象还没有被Session持久化.缓存中不存在这个对象的数据并且数据库中没有这个对象对应的数据为瞬时状态这个时候是没有OID. 持久状态:对象经过 ...

  9. 创建如下三个类:(People类中的三个方法分别输出一些信息,ChinaPeople 和AmericanPeople类重写父类的三个方法)。

    创建如下三个类:(People类中的三个方法分别输出一些信息,ChinaPeople 和AmericanPeople类重写父类的三个方法). ackage com.chuoji.text01; pub ...

随机推荐

  1. 烂泥:【解决】VMware Workstation中安装ESXI5&period;0双网卡问题

    本文由秀依林枫提供友情赞助,首发于烂泥行天下. 由于需要做ESXI相关的实验,所以就在自己的机器上利用VM虚拟ESXI进行实验.因为此次实验是需要两块网卡的,所以就在创建ESXI虚拟机时添加了两块网卡 ...

  2. unslider&period;js源码

    /** * Unslider by @idiot */ (function($, f) { // If there's no jQuery, Unslider can't work, so kill ...

  3. git add 命令添加所有改动内容

    git add xx命令可以将xx文件添加到暂存区,如果有很多改动可以通过 git add -A .来一次添加所有改变的文件. 注意 -A 选项后面还有一个句点. git add -A表示添加所有内容 ...

  4. 文件无刷新上传&lpar;swfUpload与uploadify&rpar;

    文件无刷新上传并获取保存到服务器端的路径 遇到上传文件的问题,结合之前用到过的swfUpload,又找了一个无刷新上传文件的jquery插件uploadify,写篇博客记录一下分别介绍这两个插件的实现 ...

  5. andorid开发eclipse常见问题

    1.报错: BUILD FAILED D:\workspace\ganji\build.xml:144: The following error occurred while executing th ...

  6. linux设备驱动归纳总结(一)内核的相关基础概念【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-59413.html linux设备驱动归纳总结(一):内核的相关基础概念 xxxxxxxxxxxxxx ...

  7. 368&period; Largest Divisible Subset -- 找出一个数组使得数组内的数能够两两整除

    Given a set of distinct positive integers, find the largest subset such that every pair (Si, Sj) of ...

  8. 站在K2角度审视流程--任务的独占与释放

    应用场景一:某件事情由A.B两人(或者更多人)完成,任务开始后,两人随时可以处理任务,只需有一人处理完成,此事情即可结束. 应用场景二:某件事情由A.B两人(或者更多人)完成,任务开始后,两人随时可以 ...

  9. 用netsh wlan命令行解决&OpenCurlyDoubleQuote;Win10下WLAN不自动登陆”问题

    系统崩溃了,找了一个版本Windows 10重装后,发现进入系统后不会自动连接自己家的Wifi,每次都要手动点"登录",烦不胜烦. 于是百度.Google一起上,找解决方案,然后所 ...

  10. SVN服务端和客户端的安装与搭建

    版权声明:本文为博主原创文章,转载请注明原文出处. https://blog.csdn.net/zzfenglin/article/details/50931462 SVN简介 SVN全名Subver ...