[2018.10.24]Splay伸展树(数据结构)

xryjr233

2018-10-24 19:16:09

Personal

今天学的第二种树了。(下午学的主席树->[主席树](https://www.luogu.org/blog/xryjr233/post-20181024-zhu-xi-shu-shuo-ju-jie-gou-post)) 从前,有一种东西叫做二叉搜索树(Binary Search Tree),它可以支持**期望**单次$O(logn)$的查找、插入、删除等操作。 但是,期望。。。这个词在OI里是要命的。你知道世界上所有的出题人都知道怎么卡掉它。 所以它就衍生出各种优化(AVL树、Treap、Splay等)。其中,我们神奇的Tarjan奆佬发明了Splay(这位奆佬还发明了一堆图论算法和数据结构,证明了一堆结论)。 要想了解Splay在干什么,我们需要知道BST在做什么,为什么会被卡。 BST是一棵二叉树(废话),它满足一个特殊的性质:任何一个节点的左子树的所有节点权值都小于这个节点,右子树的所有节点权值都小于这个节点。 当插入一个节点时,它可以根据这个性质确定这个节点应该在的位置。比如在插入顺序如下时,它的复杂度是很优秀的$O(logn)$: ![图炸了QAQ](https://image.ibb.co/fkRBJA/image.png) 但是,当插入顺序如下时,它的时间复杂度变为了$O(n)$: ![图炸了QAQ](https://image.ibb.co/drffdA/image.png) 所以,Splay(和它的兄弟姐妹们)就出现了。 为了维护树的平衡,Splay使用了splay操作,这也是它名字的由来。 Splay操作由若干次旋转(rotate)来实现。旋转操作的用意是:将一个节点旋转到它的父亲的位置上,并且不改变其大小关系。 一、旋转操作rotate(x) 意思是将**编号**为x的节点旋转到它的父亲的位置上。 假设x的父亲节点为y,祖父节点为z。x为y的a儿子(0左1右),y为z的b儿子(0左1右),$\bigoplus$为二进制的异或运算。 则操作如下: 1.z的b儿子变为x; 2.y的a儿子变为x的(a$\bigoplus$1)儿子; 3.x的(a$\bigoplus$1)儿子变为y。 code: ```cpp void Rotate(int x){//d为Splay的结构体数组,结构体中的c[0]为左儿子,c[1]为右儿子,f为父节点,val为点的权值。 int f=d[x].f;//f为x的父亲 int gf=d[f].f;//gf为x的祖父(不是女朋友) int ws=(d[f].c[1]==x),gws=(d[gf].c[1]==f);//ws为x是y的什么儿子,gws为y是z的什么儿子,仔细想想就能看懂了 d[gf].c[gws]=x; d[x].f=gf; d[f].c[ws]=d[x].c[ws^1]; d[d[x].c[ws^1]].f=f; d[x].c[ws^1]=f; d[f].f=x;//记得在更新子节点后更新父节点 } ``` 二、伸展操作splay(x,fa) 意思是将**编号**为x的节点伸展为**编号**为fa的节点的儿子。 假设x的父亲节点为y,祖父节点为z,x为y的a儿子(0左1右),y为z的b儿子(0左1右)。具体操作如下: 1.假如y是fa,结束; 2.假如z是fa,则对x进行旋转; 3.假如a$\ne$b,则连续对x进行两次旋转,并再次开始该操作; 4.假如a$=$b,则先对y进行一次旋转,再对x进行一次旋转,并再次开始该操作。 code: ```cpp void Splay(int x,int fa){//将x伸展到fa的儿子,fa=0则旋转到根 while(d[x].f!=fa){//重复直到fa为x的父亲 if(d[d[x].f].f==fa){//单旋 Rotate(x); }else{ int f=d[x].f; int gf=d[f].f; int ws=(d[f].c[1]==x),gws=(d[gf].c[1]==f); if(ws==gws){//a等于b Rotate(f); }else{//a不等于b Rotate(x); } Rotate(x);//两者的共同操作 } } if(!fa){//如果旋转到根,更新根 rt=x; } } ``` 三、查找操作find(x) 意思是寻找**权值**为x的节点并旋转到根,方便之后的操作。 根据BST的性质直接向下寻找,找到权值等于x的点时或不能再向下时停止查找,将当前节点伸展到根。 code: ```cpp void Find(int x){//查找权值为x的节点 int nd=rt;//开始为根 if(!nd){//空树 return; } while(d[nd].val!=x){//直到找到x if(d[nd].val>x){ if(!d[nd].c[0]){//不能向下了 break; } nd=d[nd].c[0];//跳到下一节点 }else{ if(!d[nd].c[1]){//不能向下了 break; } nd=d[nd].c[1];//跳到下一节点 } } Splay(nd,0);//伸展到根 } ``` 四、插入操作insert(x) 意思是往Splay里插入一个**权值**为x的节点。 类似查找操作向下寻找,找到时退出或者更新数量(视题目而定),如果走到空节点就在此新建一个节点。 另外,为了维护平衡,新建节点后将它伸展到根。 code: ```cpp void Insert(int x){//插入权值为x的节点 int nd; if(!rt){//空树,直接作为根 rt=++cnt; d[rt].f=0; d[rt].c[0]=0; d[rt].c[1]=0; d[rt].val=x; nd=cnt; }else{ nd=rt;//从根开始 while(d[nd].val!=x){//查找x if(d[nd].val>x){ if(!d[nd].c[0]){//找不到,新建节点 d[nd].c[0]=++cnt; break; } nd=d[nd].c[0]; }else{ if(!d[nd].c[1]){//找不到,新建节点 d[nd].c[1]=++cnt; break; } nd=d[nd].c[1]; } } if(d[nd].val!=x){//如果需要新建节点 d[cnt].f=nd;//初始化节点信息 d[cnt].c[0]=0; d[cnt].c[1]=0; d[cnt].val=x; nd=cnt; } } Splay(nd,0);//伸展到根以维护平衡 } ``` 五、前驱/后继操作last(x)/next(x) 意思是求Splay中**权值**小于/大于x的最大/最小的节点。 将**权值**为x的节点伸展到根(如果存在的话),则前驱为左子树(小于x)中最大的节点,一直跳右儿子即可。或者伸展后根的权值小于x,则根就是前驱。 后继为右子树(大于x)中最小的节点,一直跳左儿子即可。或者伸展后根的权值大于x,则根就是后继。 前驱后继有可能不存在。 code: ```cpp int Next(int x,int op){//op=0求x的前驱,op=1求后继 Find(x); if(op==0&&d[rt].val<x){//根就是前驱 return rt; } if(op==1&&d[rt].val>x){//根就是后继 return rt; } int nd=d[rt].c[op];//跳到左/右子树 if(!nd){//不存在 return -1; } while(d[nd].c[op^1]){//一直跳右/左儿子 nd=d[nd].c[op^1]; } return nd; } ``` 六、删除操作delete(x) 意思是删除**权值**为x的节点(有时是出现次数-1,视具体题目而定)。 显然大于x的前驱小于x的后继的数是x。 反应到Splay里,将前驱节点伸展到根,将后继节点伸展到根(前驱节点)的右儿子,后继节点的左儿子就是权值为x的节点(如果存在的话)。 code: ```cpp void Delete(int x){//删除权值为x的节点 int lst=Next(x,0);//前驱 int nxt=Next(x,1);//后继 Splay(lst,0);//前驱伸展到根 Splay(nxt,lst);//后继伸展为前驱的右儿子 d[nxt].c[0]=0;//删除 } ``` 还有一些合并、拆分什么的到时候再说吧。。。