算法简介
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