算法简介
Splay是一种平衡树,支持插入、删除、求排名、求第\(k\)大数、求前驱和求后继的操作,并且它还能做到一般平衡树做不到的区间操作。
定义与性质
先说二叉查找树:就是把所有数建在树上,且左边的数永远小于右边的。
对于上面说的那6个操作,其实在数据随机时二叉查找树时最强的,但是数据一条链你就Good Game了。
这种情况我们希望这棵二叉查找树的节点深度差不要太大,这就有了平衡树。
顾名思义,平衡树是平衡的二叉查找树,意思就是说1条链这种数据对于平衡树来说完全不存在,这样复杂度就有保证了。
基础操作
核心操作
这些都是很重要的操作,直接维护了Splay的平衡。
pushup
维护子树大小,很简单,不谈。好像只在旋转操作中出现。
inline void pushup(int x)
{
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+ct[x];
}
左右旋
核心中的核心,是另外一个核心操作的基础核心操作。
这个东西可以改变两个相邻节点的父子关系,并且仍然满足平衡树的性质。给张图,就可以看明白了。
inline void rota(int x)
{
int y=ff[x],z=ff[y],k=(x==ch[y][1]);
ch[z][y==ch[z][1]]=x;
ff[x]=z;
ch[y][k]=ch[x][k^1];
ff[ch[x][k^1]]=y;
ch[x][k^1]=y;
ff[y]=x;
pushup(x),pushup(y);
}
splay
为什么Splay要叫Splay?因为这个操作。这个操作就是直接保证了Splay的复杂度。
这个操作就是把一个节点旋转为目标节点的子节点,或者旋转到根节点。
这里情况有点小多(写成代码就短了,毕竟这个代码经过了一代又一代压行先辈的优化):
若该节点的爷爷节点为目标节点,直接把这个节点旋上去。
若爷爷节点不为目标节点,且该节点、父节点、爷爷节点三点一线,那么先旋父节点,再旋该节点。
若爷爷节点不为目标节点,且该节点、父节点、爷爷节点三点不共线,那么旋2次该节点。
inline void splay(int x,int father)
{
while(ff[x]!=father)
{
int y=ff[x],z=ff[y];
if(z!=father)
(y==ch[z][0])^(x==ch[y][0])?rota(x):rota(y);//压行神器
rota(x);
}
if(!father)
root=x;
}
6个操作
其实就跟二叉查找树差不多。
插入
我们给每个节点设计数器,表示这个节点代表的数有多少个。
先从根节点开始找这个数,每次都要记录目前节点及其父亲。
找到了(最后记录目前节点的变量不为\(0\))就说明这个数存在,存在就把该节点计数器\(+1\)。
没找到,那么我们可以肯定刚才的父亲可以作为新数的父亲,于是新数就变成它儿子了。注意,若父亲为\(0\),则新数为根。
最后要把新数旋为根节点,保证随机性。
inline void Insert(int x)
{
int now=root,father=0;
while(now&&x!=val[now])
{
father=now;
now=ch[now][x>val[now]];
}
if(now)
ct[now]++;
else
{
now=++cnt;
if(!father)
root=now;
else ch[father][x>val[father]]=now;
ff[now]=father,val[now]=x,ct[now]=1,siz[now]=1;
}
splay(now,0);
}
删除
先找前驱、后继的节点编号(下面会讲,可以先看下面),然后把前驱旋为根节点,后继旋为根节点的右儿子,那么此时待删除节点就被卡在后继的左儿子那儿了。
之后还是看看计数器,如果大于\(1\),那么就直接\(-1\)之后旋转该点到根节点保证随机,否则干净利落地砍掉。
为了防止找不到前驱后继,我们一开始就插入\(-\inf\)以及\(\inf\)。
void Delete(int x)
{
int xp=qpre(x),xs=qsuf(x);
splay(xp,0),splay(xs,xp);
int now=ch[xs][0];
if(ct[now]>1)
{
ct[now]--;
splay(now,0);
}
else ch[xs][0]=0;
}
查询第\(k\)大数
从根节点节点开始走,如果该节点的左子树的大小大于\(k\),那么继续往左子树走。
否则如果该节点左子树的大小加上该点计数器小于\(k\),答案就是该节点的值。
否则往左边走,同时\(k\)要减去该节点计数器以及该节点左子树大小。
inline int kth(int x)
{
int now=root;
if(siz[now]<x)
return 0;
while(1)
{
if(siz[ch[now][0]]>=x)
now=ch[now][0];
else if(siz[ch[now][0]]+ct[now]>=x)
return val[now];
else x-=siz[ch[now][0]]+ct[now],now=ch[now][1];
}
}
查询排名
这个直接先找到该数所在节点,然后答案就是左子树大小\(+1\)。
记得最后还要\(-1\),因为你插入了\(-inf\)。
inline int qrank(int x)
{
findx(x);
return siz[ch[root][0]]+1;
}
前驱
找到该数节点,然后从左儿子开始一直走其右儿子,最后到的就是前驱。
为了避免找不到的问题,先插入,再删除。
inline int qpre(int x)
{
findx(x);
int now=ch[root][0];
while(ch[now][1])
now=ch[now][1];
return now;
}
后继
同理,不多扯了。
inline int qsuf(int x)
{
findx(x);
int now=ch[root][1];
while(ch[now][0])
now=ch[now][0];
return now;
}
它活着的意义?
我们发现,这个东西常数贼大,还长。显然,我们面对上面6种操作,用treap不行吗?
对,是。但是根据黑格尔的存在即合理理论,dzy大佬花很多时间把这个东西教给我们,教练让我花很多时间把这个东西学会,我花很多时间把这个东西学会了并写出来,评测机花很多时间去评测,那这个东西肯定在别的方面更加突出。
是的,Splay可以处理区间问题(常常是连线段树也解决不了的),而别的平衡树面对区间问题捉襟见肘。
我们处理区间问题,首先直接递归建树,建树时,节点间的大小关系不再是储存的值的关系,而是节点序号的关系。这里我们就要把节点编号设为节点在原序列中的位置。
于是我们发现,某子树的中序遍历,就为一个区间(有时可以反过来化树为区间,多用于DP)。这时每个节点就可以代表该子树的一段区间了。你还可以在节点上打lazy标记优化时间复杂度。
但是说到这里,我们发现几乎所有平衡树都有这个性质啊!那么为什么一般的平衡树很难处理区间问题?
问题在于,你很难从平衡树上找到这个区间。像treap,你怎么找?
但是Splay能找。对于区间\(l~r\),只需要把\(l-1\)旋到根节点,把\(r+1\)旋到根节点的右儿子,那么根节点左儿子就是要的区间。这样一个区间就被提取出来了。
事实上,一个平衡树能否能处理区间问题,就要看能否快速提取区间。于是,我们发现好像只有Splay和FHQ无旋treap(常数更小)可以做到。于是好像只有它们可以同时做到区间问题和6种操作。
事实上,还有一种数据结构也可以,那就是树状数组,不过它是主要处理区间问题,由于某些原因可以搞6种操作,在此就不提了(其实是我不会树状数组处理6种操作)。
应用
Splay可以用于LCT。尽管Splay常数比FHQ大,但是在LCT里面,不知道为什么它复杂度更小,所以用它。
代码
1、普通平衡树(6种操作)
#include<bits/stdc++.h>
using namespace std;
int n,q,root,cnt,a[100001],ff[100001],ch[100001][2],val[100001],siz[100001],ct[100001];
inline int Read()
{
int x=0,f=1;
char cha=getchar();
while(!isdigit(cha))
{
if(cha=='-')
f=-1;
cha=getchar();
}
while(isdigit(cha))
{
x=(x<<3)+(x<<1)+cha-'0';
cha=getchar();
}
return x*f;
}
inline void pushup(int x)
{
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+ct[x];
}
inline void rota(int x)
{
int y=ff[x],z=ff[y],k=(x==ch[y][1]);
ch[z][y==ch[z][1]]=x;
ff[x]=z;
ch[y][k]=ch[x][k^1];
ff[ch[x][k^1]]=y;
ch[x][k^1]=y;
ff[y]=x;
pushup(x),pushup(y);
}
inline void splay(int x,int father)
{
while(ff[x]!=father)
{
int y=ff[x],z=ff[y];
if(z!=father)
(y==ch[z][0])^(x==ch[y][0])?rota(x):rota(y);
rota(x);
}
if(!father)
root=x;
}
inline void Insert(int x)
{
int now=root,father=0;
while(now&&x!=val[now])
{
father=now;
now=ch[now][x>val[now]];
}
if(now)
ct[now]++;
else
{
now=++cnt;
if(!father)
root=now;
else ch[father][x>val[father]]=now;
ff[now]=father,val[now]=x,ct[now]=1,siz[now]=1;
}
splay(now,0);
}
inline void findx(int x)
{
int now=root;
if(!now)
return;
while(ch[now][x>val[now]]&&val[now]!=x)
now=ch[now][x>val[now]];
splay(now,0);
}
inline int qpre(int x)
{
findx(x);
int now=ch[root][0];
while(ch[now][1])
now=ch[now][1];
return now;
}
inline int qsuf(int x)
{
findx(x);
int now=ch[root][1];
while(ch[now][0])
now=ch[now][0];
return now;
}
void Delete(int x)
{
int xp=qpre(x),xs=qsuf(x);
splay(xp,0),splay(xs,xp);
int now=ch[xs][0];
if(ct[now]>1)
{
ct[now]--;
splay(now,0);
}
else ch[xs][0]=0;
}
inline int kth(int x)
{
int now=root;
if(siz[now]<x)
return 0;
while(1)
{
if(siz[ch[now][0]]>=x)
now=ch[now][0];
else if(siz[ch[now][0]]+ct[now]>=x)
return val[now];
else x-=siz[ch[now][0]]+ct[now],now=ch[now][1];
}
}
inline int qrank(int x)
{
findx(x);
return siz[ch[root][0]]+1;
}
int main()
{
q=Read();
int opt,k;
Insert(2147483647),Insert(-2147483647);
while(q--)
{
opt=Read(),k=Read();
if(opt==1)
{
Insert(k);
}
else if(opt==2)
{
Delete(k);
}
else if(opt==3)
{
printf("%d\n",qrank(k)-1);
}
else if(opt==4)
{
printf("%d\n",kth(k+1));
}
else if(opt==5)
{
Insert(k);
printf("%d\n",val[qpre(k)]);
Delete(k);
}
else
{
Insert(k);
printf("%d\n",val[qsuf(k)]);
Delete(k);
}
}
return 0;
}
2、文艺平衡树(区间翻转)
还没写,先鸽着。
原文地址:https://www.cnblogs.com/SKTT1Faker/p/12075275.html