Loading

平衡树——Splay

0. 前言

前置知识:二叉查找树(BST)。
我们知道,BST 是会被恶意数据卡成\(O(n^2)\)的。(如下图)

树变成了一条链,这很显然不是我们想要的,我们需要找到一种方法来让 BST 保持平衡。
于是我们就有了各式各样的平衡树,而在 OI 中应用最为广泛的就是 Splay 和 fhq_treap 了,并且这两个数据结构互相不能替代。特点如下:
Splay:码量较大,常数较大,能够进行序列操作,LCT 的专用树,不能可持久化。
fhq_treap:码量适中(比 Splay 短一些),常数较大,能够进行序列操作,维护 LCT 的时间复杂度多一只 log(但是可以写 ETT),可以可持久化。

这篇文章我们主要讨论 Splay 作为单纯的平衡树时的操作。

1. 树旋转

Splay 使用树旋转来保持平衡。
如图,\(x\) 为当前结点,\(f\)\(x\) 的父亲,\(g\)\(f\) 的父亲。\(A,\ B,\ C,\ D\) 为子树。
从上图到下图,我们把 \(x\) 旋转到了\(f\) 的位置。我们称其为左旋,右旋同理。
我们称下图中的左旋为"将 \(x\) 旋转到父结点",右旋为"将 \(f\) 旋转到父结点"。
换言之,旋转谁就是说将哪个结点提升到其父结点的位置。

不妨用 \(\text{connect}(u,v)\) 表示连接 \(u,v\) 且使 \(v\)\(u\) 的父亲,同时 \(u,v\) 原有的父子关系被删除。
记节点 \(x\) 的父亲为 \(f\),祖父为 \(g\)\(x\) 的靠近 \(f\) 的儿子为 \(u\),并且现在要旋转结点 \(x\)
我们可以分为以下三步:

  1. \(\text{connect}(u,f)\)
  2. \(\text{connect}(x,g)\)
  3. \(\text{connect}(f,x)\)

图上看就是这样:

上图中演示的是左旋,右旋同理。

2. Splay 操作

利用树旋转,我们可以实现 Splay 的另一个非常重要的操作:Splay 操作。
这个操作能够把一个结点 \(x\) 旋转到 \(top\) 结点的子结点的位置(如果给定 \(top=0\) 则认为是旋转到根结点),让 Splay 保持大致平衡。
(事实上,因为这个操作,我们必须将相同的值合并到一个结点上)
看上去 Splay 操作十分简单,只需要不停地将当前结点向上旋转就行了。我们可以自己来模拟一下^_^
如图,这是一个由 \(1,2,3,4,5\) 组成的链。我们现在尝试把 \(1\) 旋转到根结点:

额(⊙o⊙)…还是一条链(╯‵□′)╯︵┻━┻
这种做法又被称为单旋,也就是每次只关心当前结点及其父结点。可以看出单旋是不能保证复杂度的(用单旋写的 Splay 又被称为 Spaly,会被卡)

由此,我们引入双旋的概念。双旋就是根据当前结点 \(x\),父结点 \(f\) 和祖父结点 \(g\) 的形态(如果没有祖父结点,那么做一次单旋就行了)来决定具体的旋转方案。具体分为两类:
异构调整:\(x,f,g\) 不在一条线上。此时操作和单旋一样,只需要将当前结点旋转两次即可(如图)。

同构调整:\(x,f,g\) 在一条线上。此时我们需要先旋转 \(f\),再旋转 \(x\)(如图)。

单独看好像区别不大。我们还是用刚才的例子来感受一下单旋和双旋的不同。

可以看到,经过两次双旋(后一次双旋将两步分开来画了,看得更清楚),树的形态发生了变化,不只是一条链了。
这样双旋虽然不能把树变得非常平衡,但是均摊复杂度是正确的。这里对 Splay 的均摊时间复杂度有更详细的分析。

3. 删除操作

事实上,有了 Splay 操作和 BST 的知识,就已经可以基本实现 Splay 了。
具体地,每次插入结点最后要将这个结点 Splay 到根结点;根据值查排名,根据排名查值和寻找前驱后继在寻找到最终结点后都要将结点 Splay 到根结点。
但是删除操作就比较复杂了。具体操作如下:
首先,我们把当前结点 Splay 到根结点。
如果相同值的节点不止一个,那很简单,只要将值的数量减一就行了;
如果当前结点没有右儿子,也就是没有后继,那只需要将根节点的指针指到左儿子上就行了;
否则,我们找到当前结点的后继并将其 Splay 到当前结点的儿子结点处,此时这个后继一定没有左儿子。于是我们直接将后继设为根结点,并 \(connect\) 后继和当前节点的左儿子就行了。
对于最后一种情况画图如下:
Splay 如图,现在我们要删掉 \(2\)

首先,我们把 \(2\) Splay 到根结点。

很明显 \(2\) 有右子树,也就是有后继。我们从右子树的根开始不停地找左子树,最终找到 \(3\)\(2\) 的后继。于是我们把 \(3\) Splay 到 \(2\) 的子节点处。

\(3\) 没有左儿子了。现在我们可以直接把 \(2\) 删掉了。具体地,让 \(2\) 的左儿子 \(1\) 成为 \(3\) 的左儿子。

删除操作完成。

4. 代码实现

压行警告。

Splay 的本体需要储存以下信息:

int tot,rt;//tot是内存池计数,rt是根
struct node{int fa,ch[2],val,cnt,siz;}tree[maxn];//fa是父亲,ch[0]是左儿子,ch[1]是右儿子,val是值,cnt为值的个数,siz为子树大小

首先,为了减少代码量,我们可以定义以下几个有用的函数:

inline bool dir(int x,int f){return tree[f].ch[1]==x;}//判断x是f的哪个儿子
inline void con(int x,int f,int s){tree[f].ch[s]=x;tree[x].fa=f;}//将f的s儿子设为x,即connect函数
inline void upd(int x){tree[x].siz=tree[tree[x].ch[0]].siz+tree[tree[x].ch[1]].siz+tree[x].cnt;}//更新siz的值
inline void newnode(int &x,int f,int val){x=++tot;tree[x]=(node){f,{0,0},val,1,1};}//新建一个结点,使用引用返回

树旋转:

inline void rotate(int x)
{
    int f=tree[x].fa,g=tree[f].fa,k=dir(x,f);
    con(tree[x].ch[k^1],f,k);con(x,g,dir(f,g));con(f,x,k^1);//写的很抽象,但也可以看出用ch[0],ch[1]的简洁
    upd(f);upd(x);//记得更新siz
}

Splay操作:

inline void splay(int x,int top)
{
    if(!top)rt=x;//前面说了,top为0则Splay到根结点
    while(tree[x].fa!=top)
    {
        int f=tree[x].fa,g=tree[f].fa;
        if(g!=top)dir(x,f)^dir(f,g)?rotate(x):rotate(f);//判断同构异构,即判断两个方向是否相同。这里使用异或操作来简便判断。
        rotate(x);
    }
}

接下来就是一个操作一个操作地写了。
插入:

void ins(int val,int &now,int fa)//传引用是好的
{
    if(!now){newnode(now,fa,val);splay(now,0);}//记得要Splay到根结点
    else if(val<tree[now].val)ins(val,tree[now].ch[0],now);
    else if(val>tree[now].val)ins(val,tree[now].ch[1],now);
    else{tree[now].cnt++;splay(now,0);}//记得要Splay到根结点
}

删除:

void delnode(int x)//删除单点
{
    splay(x,0);//先Splay到根结点
    if(tree[x].cnt>1)tree[x].cnt--;
    else if(tree[x].ch[1])
    {
        int now=tree[x].ch[1];
        while(tree[now].ch[0])now=tree[now].ch[0];//找后继
        splay(now,x);con(tree[x].ch[0],now,0);//将后继Splay到当前结点的儿子结点处
        rt=now;tree[now].fa=0;upd(rt);//记得更新siz
    }
    else{rt=tree[x].ch[0];tree[rt].fa=0;}
}
void del(int val,int now)//根据val找到点的标号,再删
{
    if(val==tree[now].val)delnode(now);
    else if(val<tree[now].val)del(val,tree[now].ch[0]);
    else del(val,tree[now].ch[1]);
}

查值查排名,前驱后继:不多解释。

int getrank(int val)
{
    int now=rt,rk=1;
    while(now)
    {
        int lsiz=tree[tree[now].ch[0]].siz;
        if(val==tree[now].val){rk+=lsiz;splay(now,0);break;}//记得要Splay到根结点
        else if(val<tree[now].val)now=tree[now].ch[0];
        else{rk+=lsiz+tree[now].cnt;now=tree[now].ch[1];}
    }
    return rk;
}
int getnum(int rk)
{
    int now=rt;
    while(now)
    {
        int lsiz=tree[tree[now].ch[0]].siz;
        if(lsiz+1<=rk&&rk<=lsiz+tree[now].cnt){splay(now,0);break;}//记得要Splay到根结点
        else if(rk<=lsiz)now=tree[now].ch[0];
        else{rk-=lsiz+tree[now].cnt;now=tree[now].ch[1];}
    }
    return tree[now].val;
}
int getpre(int x){return getnum(getrank(x)-1);}
int getsuc(int x){return getnum(getrank(x+1));}
posted @ 2021-07-12 21:50  pjykk  阅读(154)  评论(0编辑  收藏  举报