前言
Splay是一种比较难以理解的(在我看来)平衡树.
而之后还会有
R
e
d
a
n
d
B
l
a
c
k
,
A
V
L
,
S
c
a
p
e
g
o
a
t
.
.
.
Red\ and\ Black,AVL,Scapegoat...
Red and Black,AVL,Scapegoat...
想想都可怕.
之后可能还会有这些平衡树的模板.
这样就构成了平衡树的半壁江山.?
预备知识
BST
需要了解一下基础性质,各种操作和旋转机制.
Treap的缺点
在关于Treap的模板中,提起了BST的缺点,我们就用Treap填补了这种种缺点.
但是,Treap也存在缺点!
为什么?因为有神奇而美妙的
r
a
n
d
(
)
rand()
rand().
这个伪随机函数其实是一个"看脸"的函数.
什么意思?
万一出题人很毒瘤题目数据比较诡异,导致这个rand()函数做出很多不满意的结果,那么即使结合二叉堆也是不可以的.
所以有句话说的好:
靠别人不如靠自己.
于是,简单粗暴不好写的
S
p
l
a
y
Splay
Splay便产生了.
Splay
关于
S
p
l
a
y
Splay
Splay的具体实现方式,各有异同.
所以就造成了各种各样的
W
A
,
T
L
E
WA,TLE
WA,TLE…
个人建议还是只看一个blog足矣,免得造成混乱.
好了正题开始…
预备操作
首先,与Treap类似,Splay的数据域如下:
struct SPLAY
{
int ch[2];//ch[0]是左孩子,ch[1]是右孩子
int cnt,size;
int key,f;//f是父结点
};
这里什么用
c
h
ch
ch表示左右孩子?因为我们要为后面的旋转操作铺路.
还有更新
s
i
z
e
size
size的操作
U
p
d
a
t
e
Update
Update(和Treap一样):
void Update(int root)
{
a[root].size=a[a[root].ch[0]].size+a[a[root].ch[1]].size+a[root].cnt;
}
额外提供三个操作,为了简化代码.
一个是方向指示函数
d
i
r
dir
dir,指出这个孩子是父结点的哪一个孩子:
int dir(int root)
{
return (a[a[root].f].ch[0]==root)?0:1;
}
其中
0
0
0代表左孩子,
1
1
1代表右孩子.
这样就可以看到用ch表示左右孩子的优势了.
还有一个,是返回应该递归到哪一个子树中的判断函数:
int d(int root,int val)
{
return (val>a[root].key)?1:0;
}
最后一个是后面要旋转的辅助操作 C o n n e c t Connect Connect,用来连接两个结点.
void Connect(int Son,int F,int k)
{
a[Son].f=F;
a[F].ch[k]=Son;
}
这样可以开始Splay的关键操作——旋转.
旋转
Splay的旋转比Treap的操作更为复杂.
因为失去了二叉堆的性质,所以必须通过不断旋转保证平衡.
这样单纯的左旋右旋就不能满足这个性质了.
比如一条单纯的全是左孩子的链.
经过若干次右旋竟然…还是一样的深度…
这个就…有点尴尬…
因此,我们采取二次旋转的措施.
具体就是:同时旋转其父结点.
但是这样,就有四种不同的情况.
写四个旋转函数比较麻烦,并且不好选择.
所以,我们可以从旋转找规律.
举个例子,这里是旋转前的树:
这里是旋转后的树:
可以看到边数是一样的.
那么观察被改动的边,有:
- b—e被连接,a—e被断开.
- a—b被改成了b—a.
- b的父结点的对应孩子变成了a.
并且我们惊喜地发现,这三个操作是互不干扰的.
所以,旋转
R
o
t
a
t
e
Rotate
Rotate操作还可以这样表述:
//*下面称 x是其父亲的 “p” 孩子(
p
∈
{
0
,
1
}
p\in\{0,1\}
p∈{0,1}).
将对于要向上旋转的结点
x
x
x.
- 将x的祖父结点改为与 x x x连接.
- 将x的 p x o r 1 p\ xor\ 1 p xor 1孩子改为x的父亲结点.
- 将x的父结点的 p p p孩子改为x的 p p p孩子.
- 最后Update x和x的父结点.
可能有点绕,给出代码如下:
void Rotate(int x)
{
int y=a[x].f,z=a[y].f;
int dx=dir(x),dy=dir(y);
int k=a[x].ch[dx^1];
Connect(k,y,dx);
Connect(y,x,dx^1);
Connect(x,z,dy);
Update(y),Update(x);
}
通过Rotate操作可以做什么?
在Treap中曾提到,单纯的随机旋转并不能使BST平衡.会更乱
所以我们就有了维持平衡的关键操作——Splay(x,R).
x,R都是结点.
这个操作将x旋转至R的位置( 前提是 x 在 R 的子树中 ).
这个操作通常将x移到根节点,但有时也会将x移至其他地方.
有了封装的Rotate操作,我们一路将
x
x
x Rotate旋转上来.
在这里我们就可以二次旋转
x
x
x.
但是,如果只是这样操作,祖父,父亲和本结点三点共线时…就很尴尬…
因为如果画图观察,有一条链的深度没有改变.
所以我们就先旋转父节点,再旋转子结点.
为了防止父结点是根节点,而祖父结点溢出的结点.
我们设立一个虚拟结点
a
[
0
]
a[0]
a[0],并令任意一个子节点指向根结点.
下文我们用
a
[
0
]
.
r
a[0].r
a[0].r表示根结点.
void Splay(int x,int R)
{
R=a[R].f;
while (a[x].f!=R)
{
int y=a[x].f;
if (a[y].f!=R)
{
if (dir(x)==dir(y)) Rotate(y);//三点共线
else Rotate(x);
}
Rotate(x);
}
}
接下来的操作均依靠Splay保持平衡.
基本操作
首先是基本的新建结点,这个操作和Treap和BST的新建操作类似:
int New(int val,int F)//F是父结点
{
a[++cnt].f=F;
a[cnt].key=val;
a[cnt].cnt=a[cnt].size=1;
return cnt;
}
然后是查找操作.
通常的查找操作是返回待查找结点的编号.
而与这一类查找操作不同,Splay 将待查找结点旋转至根节点.
int Find(int val)
{
int p=Root;
while (1)
{
if (a[p].key==val)
{
Splay(p,Root);
return p;
}
int k=d(p,val);
if (a[p].ch[k]==0) return 0;
p=a[p].ch[k];
}
}
接下来是插入操作:
分为如下两种情况:
- 原树是空树,直接将虚拟结点与新结点连接即可.
- 否则,向下寻找应插入的位置,
如果有这个结点,直接将 c n t + 1 cnt+1 cnt+1;
否则就会找到空子树,新节点就应安放在空子树中.
最后要将插入的结点旋转至根节点,还要在一路加上子树 s i z e size size.
void Insert(int val)
{
int p=Root;
if (p==0) New(val,0),Root=cnt;
else while (1)
{
a[p].size++;
if (a[p].key==val)
{
a[p].cnt++;
Splay(p,Root);
return;
}
int k=d(p,val);
if (a[p].ch[k]==0)
{
int R=New(val,p);
a[p].ch[k]=R;
Splay(R,Root);
return;
}
p=a[p].ch[k];
}
}
接下来是删除操作.
这里我们执行Find操作,将待删除的结点旋转至根结点,
然后分三种情况讨论:
- x x x结点的 c n t > 1 cnt>1 cnt>1,直接将 x x x的 c n t − 1 , s i z e − 1 cnt-1,size-1 cnt−1,size−1;
-
x
.
c
n
t
=
1
x.cnt=1
x.cnt=1,但是根节点没有左子树.
只需要将x的右孩子与虚拟结点连接即可. -
x
.
c
n
t
=
1
x.cnt=1
x.cnt=1,根节点有左子树,
那么将x的前驱(见BST)旋转至x的左孩子,然后将右子树接到前驱结点上.
因为x的前驱是x的左子树中最大的,所以它没有右子树,直接连接即可.
最后更新现在的根节点(即前驱).
void Delete(int val)
{
int p=Find(val);
if (p==0) return;
if (a[p].cnt>1)
{
a[p].cnt--;
a[p].size--;
}
else
{
if (a[p].ch[0]==0&&a[p].ch[1]==0) Root=0;
else if (a[p].ch[0]==0)
{
Root=a[p].ch[1];
a[Root].f=0;
}
else
{
int L=a[p].ch[0];
while (a[L].ch[1]>0) L=a[L].ch[1];
Splay(L,a[p].ch[0]);
Connect(a[p].ch[1],L,1);
Connect(L,0,1);
Update(L);
}
}
}
接下来是查询排名操作和查询值操作.
这里的排名是比它小的数据个数
+
1
+1
+1.
查询排名只需要执行Find(x)操作,然后返回根节点左子树的
s
i
z
e
+
1
size+1
size+1即可.
int GetRank(int val)
{
int p=Find(val);
return a[a[p].ch[0]].size+1;
}
查询值和Treap的GetRank类似(不忘Splay):
int GetVal(int x)
{
int p=Root;
while (1)
{
int k=a[p].size-a[a[p].ch[1]].size;
if (x>a[a[p].ch[0]].size&&x<=k)
{
Splay(p,Root);
return a[p].key;
}
if (x<k) p=a[p].ch[0];
else x-=k,p=a[p].ch[1];
}
}
还有求前驱和求后继,也和Treap类似(不忘Splay):
这里用了一个简化版本:
直接沿途记录更新,免去了查找的部分.
但是这里有一点需要注意:
GetPre操作如果相等要递归左子树,GetNext则为右子树,而不是单纯的
d
d
d操作.
想当年我在这里困了2小时,一把辛酸泪
Code
#include<bits/stdc++.h>
using namespace std;
#define Root a[0].ch[1]
const int N=1e5+5;
const int INF=1e9;
struct SPLAY
{
int ch[2];
int cnt,size,key,f;
}a[N];
int cnt;
void Update(int root)
{
a[root].size=a[a[root].ch[0]].size+a[a[root].ch[1]].size+a[root].cnt;
}
int dir(int root)
{
return (a[a[root].f].ch[0]==root)?0:1;
}
int d(int root,int val)
{
return (val>a[root].key)?1:0;
}
void Connect(int Son,int F,int k)
{
a[Son].f=F;
a[F].ch[k]=Son;
}
void Rotate(int x)
{
int y=a[x].f,z=a[y].f;
int dx=dir(x),dy=dir(y);
int k=a[x].ch[dx^1];
Connect(k,y,dx);
Connect(y,x,dx^1);
Connect(x,z,dy);
Update(y),Update(x);
}
void Splay(int x,int R)
{
R=a[R].f;
while (a[x].f!=R)
{
int y=a[x].f;
if (a[y].f!=R)
{
if (dir(x)==dir(y)) Rotate(y);
else Rotate(x);
}
Rotate(x);
}
}
int New(int val,int F)
{
a[++cnt].f=F;
a[cnt].key=val;
a[cnt].cnt=a[cnt].size=1;
return cnt;
}
void Insert(int val)
{
int p=Root;
if (p==0) New(val,0),Root=cnt;
else while (1)
{
a[p].size++;
if (a[p].key==val)
{
a[p].cnt++;
Splay(p,Root);
return;
}
int k=d(p,val);
if (a[p].ch[k]==0)
{
int R=New(val,p);
a[p].ch[k]=R;
Splay(R,Root);
return;
}
p=a[p].ch[k];
}
}
int Find(int val)
{
int p=Root;
while (1)
{
if (a[p].key==val)
{
Splay(p,Root);
return p;
}
int k=d(p,val);
if (a[p].ch[k]==0) return 0;
p=a[p].ch[k];
}
}
void Delete(int val)
{
int p=Find(val);
if (p==0) return;
if (a[p].cnt>1)
{
a[p].cnt--;
a[p].size--;
}
else
{
if (a[p].ch[0]==0&&a[p].ch[1]==0) Root=0;
else if (a[p].ch[0]==0)
{
Root=a[p].ch[1];
a[Root].f=0;
}
else
{
int L=a[p].ch[0];
while (a[L].ch[1]>0) L=a[L].ch[1];
Splay(L,a[p].ch[0]);
Connect(a[p].ch[1],L,1);
Connect(L,0,1);
Update(L);
}
}
}
int GetRank(int val)
{
int p=Find(val);
return a[a[p].ch[0]].size+1;
}
int GetVal(int x)
{
int p=Root;
while (1)
{
int k=a[p].size-a[a[p].ch[1]].size;
if (x>a[a[p].ch[0]].size&&x<=k)
{
Splay(p,Root);
return a[p].key;
}
if (x<k) p=a[p].ch[0];
else x-=k,p=a[p].ch[1];
}
}
int GetPre(int val)
{
int p=Root;
int ans=-INF;
while (p>0)
{
if (a[p].key<val&&a[p].key>ans) ans=a[p].key;
if (val>a[p].key) p=a[p].ch[1];
else p=a[p].ch[0];
}
return ans;
}
int GetNext(int val)
{
int p=Root;
int ans=INF;
while (p>0)
{
if (a[p].key>val&&a[p].key<ans) ans=a[p].key;
if (val<a[p].key) p=a[p].ch[0];
else p=a[p].ch[1];
}
return ans;
}
int main()
{
int T;
scanf("%d",&T);
while (T--)
{
int op,x;
scanf("%d%d",&op,&x);
switch(op)
{
case 1:Insert(x);break;
case 2:Delete(x);break;
case 3:printf("%d\n",GetRank(x));break;
case 4:printf("%d\n",GetVal(x));break;
case 5:printf("%d\n",GetPre(x));break;
case 6:printf("%d\n",GetNext(x));break;
}
}
return 0;
}
感谢奆老关注 qwq ?