我们以一道题来引入吧!

传送门

题目说的很清楚,我们的数据结构要支持:插入x数,删除x数,查询数的排名和排名为x的数,求一个数前驱后继。

似乎用啥现有的数据结构都很难做到在O(nlogn)的复杂度中把这些问题全部解决……(别跟我说什么set,vector……)

所以我们来介绍一种新的数据结构——平衡树splay!

什么是平衡树呢?这是一种数据结构,满足以下性质:

1.它是一棵二叉树

2.对于任意一个节点,它的左子树中的任意一个节点的权值比他小,右子树中任意一个节点的权值比他大

3.一棵平衡树的任意一棵子树也是一个平衡树(其实第二条已经说明了这个事了)

比如说这就是一棵平衡树(偷了yyb神犇的图),它现在非常优秀,深度是logn的。但是如果向一条链上插入多个数,或者出现什么奇怪的操作使平衡树退化为链,那么再进行查找等操作是很慢的,最坏甚至会到O(n)……比如下面这样(仍然偷了yyb神犇的图)

平衡树本身是可以支持上面所说的这些查询操作的(因为自身本身优秀的性质使得其可以进行二分),但是为了解决他有可能退化为链的这么个问题,神犇tarjan发明了一种新的数据结构——splay。

splay(伸展树),核心操作是rotate和splay(多个rotate),以使得对于每次操作之后树依然能保持一个良好的身材,使之不至于被退化为链,每次操作都能快速的在O(logn)中完成。这种操作使得每次改变之后这棵树仍然是一颗平衡树,但是节点的顺序发生了一些改变使得深度保持稳定。我们来看个图理解一下。

(以下图均偷自yyb神犇)

如图,x,y,z是三个节点,A,B,C是三棵子树。因为是一棵平衡树所以现在必然满足x<y<z.

现在我们看似乎x所在的那一侧有点长……所以我们不妨尝试把x旋转到y的位置,这样就能重构这棵树的结构,或许能使这棵树的深度变得更优。

首先我们肯定要保证新构成的树还是一棵平衡树,所以对于x,y,z三个节点,必然x,y还是在z的左子树中,既然把x移动到y的位置,那么x自然就成了z的左子树。不过平衡树还是一棵二叉树,那么y节点肯定不能呆在那了,它应该去哪呢?

我们思考一下,首先因为必须满足y>x,y<z,而x又是z的左儿子,所以y必然在x的左右子树中。而y还比x大,所以y自然应该在x的右子树中。那我们就直接把y变成x的右儿子就好啦!不过这样的话,x,y的子树又应该怎么办呢?x原来是y的左儿子,现在x成了y的父亲,那么y的左儿子那个位置肯定是空的,然后又因为x现在右儿子是y,它原来的右儿子多了出来(就是上面图的B)。B必然保证所有节点都<y,y现在还缺左儿子,所以直接把B变为y的左子树即可。

这样它就变成了这个样子,而且仍然是一棵平衡树。(上面已经说明过了,也可以自己看看验证一下)

不过其实不只有这一种情况,x,y,z之间的关系一共有四种,随便推一推画一画就能看出来。

但是我们不可能写四个函数的对吧!所以我们要找出其中的普遍性规律,使得我们写一个函数就能解决问题。

首先因为肯定是用x去代替y,那么y是z的哪个儿子,转化之后x也必然是z的哪个儿子。

之后,我们又发现,一开始x是y的哪个儿子,那么在进行一次转变之后x的那个儿子就不会变化。为什么呢?假如说现在x是y的右儿子,那么必然满足x>y.而经过一次转变之后,x成为了z的一个儿子,那么y只能成为x的其中一个儿子(这个上文已经提到过了),那么现在y<x,y自然就成为了x的左儿子,相对应的x左儿子移动,右儿子并没有变化。反过来也是一个道理。

简单的来说就一句话:x是y的哪个儿子,那么相对应的x的那个儿子一定比其父亲(x)在权值方面(大/小)更优,所以y必然不会更新到它。

最后就很容易看出来了,x是y的哪个儿子,y就会成为x与之相对应的儿子(比如x是y的左儿子,y就会成为x的右儿子)

总结一下:(直接抄yyb神犇的话了,比较简洁)

所以总结一下: 
1.X变到原来Y的位置 
2.Y变成了 X原来在Y的 相对的那个儿子 
3.Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变 
4.X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子

我们上一份代码来看一下吧!

bool get(int x)//get用于返回当前节点是自己父亲的哪个儿子
{
return t[t[x].fa].ch[] == x;
} void pushup(int x)
{
t[x].son = t[t[x].ch[]].son + t[t[x].ch[]].son + t[x].cnt;
}//更新自己为根的子树大小,其中cnt是这个数出现的次数 void rotate(int x)//旋转操作
{
int y = t[x].fa,z = t[y].fa,k = get(x);
t[z].ch[t[z].ch[] == y] = x,t[x].fa = z;//更改z节点的儿子,x节点的父亲
t[y].ch[k] = t[x].ch[k^],t[t[y].ch[k]].fa = y;//更改y节点的一个儿子和它的父亲
t[x].ch[k^] = y,t[y].fa = x;//更改x节点的儿子和y节点的父亲
pushup(x),pushup(y);//更新状态
}

是不是炒鸡好理解呀QWQ

那么splay最核心的操作我们就说完了。下面我们再来说说splay操作!(要不这玩意为啥叫splay啊……)

其实splay操作就是rotate操作的叠加,本来rotate操作是很优秀的一种操作,不过实际上还会发生一些奇怪的事情,我们要考虑这样一些问题。

比如说我们看这样一棵平衡树。

好的,如果你直接两次右rotate把x旋到z的位置,那么你将得到这样一棵平衡树:

我们仔细观察之后发现一个神奇的问题,有一条链其实没有发生长度变化。就是z-y-x-b这条链,两次旋转之后变成了z-x-y-b,但是本身的长度是没有改变的。

这样splay就容易被卡,比如你一直往这条链里面插入元素,它可能就变得很长很长很长,你查询的速度又会很慢很慢很慢。

那咋办?难道伟大的splay就不能实现了嘛?当然不是,我们只要更改一下rotate的办法即可。对于上面这种状况,我们可以自己手画一下,如果先把y旋转到z的位置,再把x旋转到y的位置,这条链的长度就缩短了。

这个也是有普遍性结论的!我们发现如果x,y是y,z的同一个儿子,那么就先旋转y再旋转x,否则直接旋转两次x即可。(这段大家手画一下理解一下更好)

可能有人会提出这样的疑问,就是在更改了rotate顺序之后,当前这条链确实缩短了,但是相对的还有一(多)条链可能没有长度变化,这个不会被卡吗?

仔细想之后是不会的,因为我们每次splay操作是从当前被修改的节点开始的。所以只要保证当前所在链长度不会增大即OK。况且从长远来看,你构造出一个splay之后,它本身是会接近满二叉树的一种状态,你对其进行一次splay,它仍然是接近满二叉树的状态,故进过多次splay之后,这棵splay依然是接近满二叉树的良好身材。

我们来看一下splay操作的代码!

void splay(int x,int goal)//goal是要旋转到的目标节点
{
while(t[x].fa != goal)
{
int y = t[x].fa,z = t[y].fa;
if(z != goal) (t[y].ch[] == x) ^ (t[z].ch[] == y) ? rotate(x) : rotate(y);//对于左右儿子的讨论
rotate(x);
}
if(goal == ) root = x;//更改根节点
}

ovo其实到这里splay最核心的俩操作已经结束了。那我们来继续说一说怎么支持上面的这些操作。

首先是查找find操作,查找一个数的位置。对于一个节点,左面全比他小,右面全比他大,那我们就可以直接愉快的二分,向左/右递归即可。然后我们把这个节点splay到根,方便接下来肆意(划死)操作。

我们直接看代码吧!

void find(int x)
{
int u = root;
if(!u) return;
while(t[u].ch[x > t[u].val] && x != t[u].val) u = t[u].ch[x > t[u].val];//通过x和当前节点值的大小比较来确定应该向哪里递归
splay(u,);
}

然后是insert操作,插入一个数。我们首先找到这个数在平衡树中哪个位置,如果这个节点已经存在的话,那我们把这个节点出现次数++,否则直接新建一个节点。

void insert(int x)
{
int u = root,f = ;
while(u && t[u].val != x) f = u,u = t[u].ch[x > t[u].val];
if(u) t[u].cnt++;
else
{
u = ++idx;
if(f) t[f].ch[x > t[f].val] = u;
t[u].ch[] = ,t[u].ch[] = ;
t[u].fa = f,t[u].val = x,t[u].cnt = ,t[u].son = ;
}
splay(u,);
}

在之后,前驱/后继操作(通过传0/1可以实现用一个函数同时找前驱/后继)。一个数的前驱就是其左子树中最靠右的节点,后继同理。

int next(int x,int f)
{
find(x);
int u = root;
if((t[u].val > x && f) || (t[u].val < x && !f)) return u;//如果当前节点权值大于x而且要找后继,或者当前节点权值小于x同时要找前驱
u = t[u].ch[f];
while(t[u].ch[f^]) u = t[u].ch[f^];//要往反方向跳
return u;
}

当然我们可以选择分开写(这个是求根的前驱后继,实际使用的时候可以转化为先求一个点的编号再这样做)

int pre()
{
int u = t[root].ch[];
while(t[u].ch[]) u = t[u].ch[];
return u;
}
int next
{
int u = t[root].ch[];
while(t[u].ch[]) u = t[u].ch[];
return u;
}

最后是删除del操作。这个还是稍微有点麻烦。我们首先找到x的前驱,把它旋转到根,再找到x的后继把它旋转到当前的根(x的前驱)的右子树,那么这个时候x的后继的左子树里面就只可能有x,把它删了。如果出现次数大于1就cnt--并且splay到根,否则直接删了完事。

void del(int x)
{
int la = next(x,),ne = next(x,);
splay(la,),splay(ne,la);
int g = t[ne].ch[];
if(t[g].cnt > ) t[g].cnt--,splay(g,);
else t[ne].ch[] = ;
}

哎不对,还有一个排名为x的数。这个也比较容易,我们还是用二分的思想即可。

int rk(int x)
{
int u = root;
if(t[u].son < x) return ;
while()
{
int y = t[u].ch[];
if(x > t[y].son + t[u].cnt) x -= (t[y].son + t[u].cnt),u = t[u].ch[];//这时要向右子树找
else if(t[y].son >= x) u = y;向左子树找
else return t[u].val;//否则说明找到了,返回
}
}

那我们就成功的用splay解决了这些问题!我们来看一下完整代码。

// luogu-judger-enable-o2
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
#include<set>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n') using namespace std;
typedef long long ll;
const int M = ;
const int INF = ; int read()
{
int ans = ,op = ;
char ch = getchar();
while(ch < '' || ch > '')
{
if(ch == '-') op = -;
ch = getchar();
}
while(ch >= '' && ch <= '')
{
ans *= ;
ans += ch - '';
ch = getchar();
}
return ans * op;
} struct node
{
int fa,son,ch[],cnt,val;
}t[M<<]; int n,root = ,idx,op,x; void pushup(int x)
{
t[x].son = t[t[x].ch[]].son + t[t[x].ch[]].son + t[x].cnt;
} bool get(int x)
{
return t[t[x].fa].ch[] == x;
} void rotate(int x)
{
int y = t[x].fa,z = t[y].fa,k = get(x);
t[z].ch[t[z].ch[] == y] = x,t[x].fa = z;
t[y].ch[k] = t[x].ch[k^],t[t[y].ch[k]].fa = y;
t[x].ch[k^] = y,t[y].fa = x;
pushup(x),pushup(y);
} void splay(int x,int goal)
{
while(t[x].fa != goal)
{
int y = t[x].fa,z = t[y].fa;
if(z != goal) (t[y].ch[] == x) ^ (t[z].ch[] == y) ? rotate(x) : rotate(y);
rotate(x);
}
if(goal == ) root = x;
} void insert(int x)
{
int u = root,f = ;
while(u && t[u].val != x) f = u,u = t[u].ch[x > t[u].val];
if(u) t[u].cnt++;
else
{
u = ++idx;
if(f) t[f].ch[x > t[f].val] = u;
t[u].ch[] = ,t[u].ch[] = ;
t[u].fa = f,t[u].val = x,t[u].cnt = ,t[u].son = ;
}
splay(u,);
} void find(int x)
{
int u = root;
if(!u) return;
while(t[u].ch[x > t[u].val] && x != t[u].val) u = t[u].ch[x > t[u].val];
splay(u,);
} int next(int x,int f)
{
find(x);
int u = root;
if((t[u].val > x && f) || (t[u].val < x && !f)) return u;
u = t[u].ch[f];
while(t[u].ch[f^]) u = t[u].ch[f^];
return u;
} void del(int x)
{
int la = next(x,),ne = next(x,);
splay(la,),splay(ne,la);
int g = t[ne].ch[];
if(t[g].cnt > ) t[g].cnt--,splay(g,);
else t[ne].ch[] = ;
} int rk(int x)
{
int u = root;
if(t[u].son < x) return ;
while()
{
int y = t[u].ch[];
if(x > t[y].son + t[u].cnt) x -= (t[y].son + t[u].cnt),u = t[u].ch[];
else if(t[y].son >= x) u = y;
else return t[u].val;
}
} int main()
{
insert(INF),insert(-INF);
n = read();
rep(i,,n)
{
op = read();
if(op == ) x = read(),insert(x);
else if(op == ) x = read(),del(x);
else if(op == ) x = read(),find(x),printf("%d\n",t[t[root].ch[]].son);
else if(op == ) x = read(),printf("%d\n",rk(x+));
else if(op == ) x = read(),printf("%d\n",t[next(x,)].val);
else if(op == ) x = read(),printf("%d\n",t[next(x,)].val);
}
return ;
}

一开始插入INF和-INF的操作其实可以暂时忽略,那个是保证splay翻转区间时的正确性,还有好多其他实际题中的应用正确性的。(区间反转下次再说)

splay是不是很好理解呢?(不过代码讲真还是比较难记……)

Splay基本操作的更多相关文章

  1. SPOJ 4487 Splay 基本操作

    插入操作,删除操作和置换操作都是单点的,所以不需要lazy标记.这个很简单,都是两次RotateTo,一次Splay操作就搞定. 求最大连续字段和的操作和线段树的题目类似,只需要保存最左边的连续最大字 ...

  2. 【BZOJ-1552&3506】robotic sort&排序机械臂 Splay

    1552: [Cerc2007]robotic sort Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 806  Solved: 329[Submit][ ...

  3. splay详解(一)

    前言 Spaly是基于二叉查找树实现的, 什么是二叉查找树呢?就是一棵树呗:joy: ,但是这棵树满足性质—一个节点的左孩子一定比它小,右孩子一定比它大 比如说 这就是一棵最基本二叉查找树 对于每次插 ...

  4. splay:优雅的区间暴力!

    万年不更的blog主更新啦!主要是最近实在忙,好不容易才从划水做题的时间中抽出一段时间来写这篇blog 首先声明:这篇blog写的肯定会很基础...因为身为一个蒟蒻深知在茫茫大海中找到一个自己完全能够 ...

  5. POJ3468:A Simple Problem with Integers (线段树||树状数组||Splay解决基本问题的效率对比)

    You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of op ...

  6. Splay&LCT

    Splay && LCT \(\text{Splay}\) 基本操作 1.\(Zig \& Zag\) 其思想是维护中序遍历不变 实现中我们不真的用\(Zig\)或\(Zag\ ...

  7. 【学术篇】NOIP2017 d2t3 列队phalanx splay做法

    我可去他的吧.... ==============先胡扯些什么的分割线================== 一道NOIP题我调了一晚上...(其实是因为昨晚没有找到调试的好方法来的说...) 曾经我以 ...

  8. hdu4217splay

    题意:有1到n的数组,每次删除第k小的值,并求和 题解:splay基本操作,删除+合并 坑点:由于不会c++指针操作,sb的只删除了头指针导致一直mle #include<bits/stdc++ ...

  9. 板子-GOD BLESS ALL OF YOU

    字符串: KMP #include<cstdio> #include<cstring> ; ]; ]; int l1,l2; void get_next() { next[]= ...

随机推荐

  1. CodeForces 21 A+B

                                                         Jabber ID 判断邮箱地址格式是否正确..一把心酸泪...跪11+,,看后台才过.. 注 ...

  2. 从Excel中读取数据(python-xlrd)

    从Excel中读取数据(python-xlrd) 1.导入模块 import xlrd 2.打开Excel文件读取数据 data = xlrd.open_workbook('excelFile.xls ...

  3. Date日期模式

    package cn.zmh.Date; import java.text.SimpleDateFormat; import java.util.Date; public class DateDemo ...

  4. Linux下使用Curl调用Java的WebService接口

    其实只要是标准的WSDL的SOA接口WebService都可以用. 调用方式: 注意:上面的方式不包括加密或者登录的,其实SOA有一套完整的加密方式. curl -H'Content-Type: te ...

  5. B/S(WEB)系统中使用Activex插件调用扫描仪实现连续扫描并上传图像(IE文件扫描并自动上传)

    IE浏览器下使用Activex插件调用客户端扫描仪扫描文件并山传,可以将纸质档案(如合同.文件.资料等)扫描并将扫描图像保存到服务器,可以用于合同管理.档案管理等. 通过插件方式调用扫描仪扫描并获取图 ...

  6. c++之函数对象、bind函数

    函数对象实质上是一个实现了operator()--括号操作符--的类. class Add { public: int operator()(int a, int b) { return a + b; ...

  7. 当Eclipse爱上SVN

    推荐使用:Subclipse  :http://jingyan.baidu.com/article/1612d5007d41e9e20e1eeeff.html 为离线安装做准备: 1.下载Subver ...

  8. unity3d 摄像机抖动特效

    摄像机抖动特效 在须要的地方调用CameraShake.Shake()方法就能够  

  9. hdu1873 看病要排队(结构体优先队列)

    看病要排队 Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Subm ...

  10. Python中暂未解决的问题

    编写一个复杂的计算器,可以在通过GUI输出出来.参考代码http://www.cnblogs.com/BeginMan/p/3216093.html shelve模块中open()函数调用文件文件的路 ...