[模板] Splay

前言

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的操作更为复杂.
因为失去了二叉堆的性质,所以必须通过不断旋转保证平衡.
这样单纯的左旋右旋就不能满足这个性质了.
比如一条单纯的全是左孩子的链.
在这里插入图片描述
经过若干次右旋竟然…还是一样的深度…
这个就…有点尴尬
因此,我们采取二次旋转的措施.
具体就是:同时旋转其父结点.
但是这样,就有四种不同的情况.
写四个旋转函数比较麻烦,并且不好选择.
所以,我们可以从旋转找规律.
举个例子,这里是旋转前的树:
在这里插入图片描述
这里是旋转后的树:
在这里插入图片描述
可以看到边数是一样的.
那么观察被改动的边,有:

  1. b—e被连接,a—e被断开.
  2. a—b被改成了b—a.
  3. 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.

  1. 将x的祖父结点改为与 x x x连接.
  2. 将x的 p   x o r   1 p\ xor\ 1 p xor 1孩子改为x的父亲结点.
  3. 将x的父结点的 p p p孩子改为x的 p p p孩子.
  4. 最后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];
    }
}

接下来是插入操作:
分为如下两种情况:

  1. 原树是空树,直接将虚拟结点与新结点连接即可.
  2. 否则,向下寻找应插入的位置,
    如果有这个结点,直接将 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操作,将待删除的结点旋转至根结点,
然后分三种情况讨论:

  1. 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 cnt1,size1;
  2. x . c n t = 1 x.cnt=1 x.cnt=1,但是根节点没有左子树.
    只需要将x的右孩子与虚拟结点连接即可.
  3. 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 ?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值