zoukankan      html  css  js  c++  java
  • Splay算法详解

    Splay算法详解

    本篇随笔浅谈一下算法竞赛中的(Splay)算法。

    Splay的概念

    Splay在我看来应该算作一种算法而非数据结构。无论是Treap,AVL,SBT,替罪羊树还是Splay其实都应该算作算法,因为它们都在解决一种数据结构存在的问题:二叉搜索树(BST)

    对于二叉搜索树和Treap(平衡树概念)不了解的,可以移步下面两篇博客:

    浅谈二叉搜索树

    详解Treap

    有了前置知识,我们可以发现问题的根源在于(BST)在数据极端的情况下会退化成链。而什么Treap啊,AVL啊,SBT啊,Splay啊,替罪羊树啊都是在干一件事情:如何让这棵树尽可能平衡,不要退化成链。

    有个叫做Tarjan的大佬YY出了Splay这种算法。

    Splay的实现方式

    Treap的实现原理在于为所有节点附加一个键值。这个键值按堆的形式排序。让整个数据结构在节点权值上是BST,但在优先级(键值)上却是一个堆。

    但Splay的实现方式是对节点的序号进行重构,从而达到平衡树要求的标准。


    splay操作

    splay操作是指将一个点一直上旋,知道某个指定点。默认是根节点。

    Splay的精髓在于旋转,这已经是不争的事实了。

    如果对于树的旋转这个知识点有些不明白,请移步Treap的博客:

    Treap详解


    相比于Treap只有单旋这一种旋转方式,Splay的优秀之处在于它有着4种旋转方式:单旋和双旋。

    什么是双旋?为什么要双旋?

    双旋的适用范围在于:祖父、父亲和儿子节点连成一条线。

    就像这样:

    1595832450131

    如果这样的结构不去双旋而简单粗暴地用两次单旋(先将需要旋转的节点上旋,再转他的父亲)来解决,最后造成的结果就像是右边的样子(自己手画模拟一下两次单旋即可发现)。我们发现,左侧的树是四层,右侧的树仍然是四层:对于复杂度来讲,没有任何优化。因为我们的平衡树是尽量把复杂度变成(log)级别的。

    那么,我们就需要使用双旋。双旋的操作方式是:先将父节点上旋,再将需要旋转的节点上旋。别看这个双旋只是改变了其旋转顺序,但是可是大有文章,经过这样的旋转策略,最终的旋转结果就变成了这样:

    1595833252570

    嗯,不错,少了一层,看起来很平衡。

    所以,我们的Splay的精华是Splay操作,也就是把某个节点一直转到根节点,顺道改变整棵树的形态。具体实现方式是:如果当前节点的父亲节点就是根节点或者自己、父亲、祖父不在一条直线上,就单旋;如果自己、父亲、祖父正好在同一条直线上,那么就双旋:双旋的策略是:先转父亲后转自己。

    所以我们有四种旋转方式:单旋左旋,单旋右旋,双旋左旋,双旋右旋。

    这也是Splay的基本操作。


    Splay和Treap在旋转上的区别

    学过Treap的小伙伴(包括我)都在觉得Splay的单旋和Treap的左右旋好像是一样的。但是实际上它们却有所不同。在本蒟蒻的理解上,这种不同体现在“对象”上。

    Treap的旋转是将儿子节点旋到自己的位置。左旋是把右儿子转到自己的位置,右旋是把左儿子转到自己的位置。

    但是Splay的旋转则是将自己转到父亲节点的位置,虽然形式上都是上旋,但是作用对象是不一样的。


    Splay的代码实现

    一般来讲,Splay支持的操作和Treap是差不多的。其实所有的平衡树都是这种操作。动态插入删除,然后去动态查询第k大、数的排名、前驱、后继等等。因为平衡树的本质是BST,所以BST所拥有的性质,Splay全都可以支持和维护。

    就拿洛谷的例题来讲吧:P3369的模板。

    题目传送门

    前置信息

    前置信息包括三个函数:maintain函数(来维护节点的size,后期统计排名用),get函数(确认当前节点是它爹的左儿子还是右儿子,旋转要用)以及find函数(在树中找到某个值的节点位置)

    void maintain(int x)
    {
        size[x]=cnt[x]+size[ch[x][0]]+size[ch[x][1]];
    }
    int find(int x)
    {
        int pos=root;
        while(val[pos]!=x)
            pos=ch[pos][x>val[pos]];
        return pos;
    }
    int get(int x)
    {
        int f=fa[x];
        return ch[f][1]==x;
    }
    

    代码很简单,应该一看就能懂。

    旋转&Splay操作

    上面的Splay的实现原理就在讲旋转和Splay操作的原理。依照上面讲的模拟即可得到下面的模板:

    void rotate(int x)
    {
        int y=fa[x],z=fa[y];
        int k=get(x);//k表示x是y的什么儿子
        ch[y][k]=ch[x][k^1],fa[ch[y][k]]=y;
        ch[x][k^1]=y,fa[y]=x,fa[x]=z;
        if(z)
            ch[z][ch[z][1]==y]=x;
        maintain(y),maintain(x);
    }
    void splay(int x,int &goal)
    {
        int f=fa[goal];
        while(fa[x]!=f)
        {
            if(fa[fa[x]]!=f)
                rotate(get(x)==get(fa[x])?fa[x]:x);
            rotate(x);
        }
        goal=x;
    }
    

    插入

    插入的时候需要一个动态开点。也就是用到哪开到哪,所以一开始首先要判整棵树是不是空的。然后再去进行插入。插入的时候要注意:因为有些数可以重复出现。所以当我们在原树中找到与插入节点完全相同的节点之后,不用去插入,直接把计数变量+1就可。

    如果这是一个全新的节点,那么它一定是个叶子。所以如果我们没在原树中找到这个节点的话,就得用动态开点把点加进去。

    动态开点不太会的小伙伴走这边:

    浅谈动态开点

    插入要注意两点:第一点是maintain,也就是统计节点的大小。第二点是Splay,每次插入和删除之后都要Splay,来维护树的形态。这也是Splay算法的精髓所在,一定不要忽略。这两种操作应该maintain在前,splay在后,因为在进入splay之前要保证当前状态下所有节点的信息都是对的,才能保证Splay之后所有节点的信息也都是对的。

    插入代码:

    void insert(int x)
    {
        if(!root)
        {
            ++tot;
            root=tot;
            val[root]=x;
            cnt[root]=1;
            maintain(root);
            return;
        }
        int pos,f;
        pos=f=root;
        while(pos && val[pos]!=x)
            f=pos,pos=ch[pos][x>val[pos]];
        if(!pos)
        {
            ++tot;
            cnt[tot]=1;
            val[tot]=x;
            fa[tot]=f;
            ch[f][x>val[f]]=tot;
            maintain(tot);
            splay(tot,root);
            return;
        }
        ++cnt[pos];
        maintain(pos);
        splay(pos,root);
    }
    

    删除

    删除的操作其实和Treap的有些像,但是又有些不同。作为一个树中节点来讲,尤其是一个BST的节点,我们肯定不能直接上来就暴力删掉这个节点,否则无法维护BST的性质。为了解决这个问题,我们选择把节点直接Splay转到根节点,这样的话,直接删根对左右子树是无影响的,直接合并就好。

    但是要分几种情况讨论:(Splay之后)

    首先,如果这个点有很多数,但是我们只删一个,所以直接cnt-1就可,其他不用变。

    如果只有一个,那么就要删除,但是删除之后谁来当根节点呢?这又有三种情况可以讨论:根节点只有左儿子,根节点只有右儿子,根节点儿女双全。

    前两种只能给唯一的儿子。如果儿女双全,就得在删掉当前根节点后再找一个根节点,但是问题来了,新的根节点到底是用左儿子合适还是用右儿子合适?

    都不合适。根据BST的性质,因为左子树都比当前节点小,右子树都比当前节点大,所以我们选择根节点左子树中最大的点作为根节点(就是根节点向左,然后一直向右到头)。

    别忘了Splay维护和maintain更新。

    代码:

    void del(int x)
    {
        int pos=find(x);
        splay(pos,root);
        if(cnt[root]>1)
        {
            cnt[root]--;
            maintain(root);
            return;
        }
        if((!ch[root][0])&&(!ch[root][1]))
            root=0;
        else if(!ch[root][0])
            root=ch[root][1],fa[root]=0;
        else if(!ch[root][1])
            root=ch[root][0],fa[root]=0;
        else
        {
            pos=ch[root][0];
            while(ch[pos][1])
                pos=ch[pos][1];
            splay(pos,ch[root][0]);
            ch[pos][1]=ch[root][1];
            fa[ch[root][1]]=pos,fa[pos]=0;
            maintain(pos);
            root=pos;
        }
    }
    

    查询排名

    当然可以用BST的性质来遍历树计算排名。但是好笨的样子。

    我们会Splay啊,直接把要查的节点Splay上去,然后直接调用其左子树的size+1不就可以了嘛!

    int rank(int x)
    {
        int pos=find(x);
        splay(pos,root);
        return size[ch[root][0]]+1;
    }
    

    查询第k大

    int kth(int x)
    {
        int pos=root;
        while(1)
        {
            if(x<=size[ch[pos][0]])
                pos=ch[pos][0];
            else 
            {
                x-=(size[ch[pos][0]]+cnt[pos]);
                if(x<=0)
                {
                    splay(pos,root);
                    return val[pos];
                }
                pos=ch[pos][1];
            }
        }
    }
    

    查询前驱/后继

    前驱和后继的定义已经给出。我们惊喜地发现和BST的性质真的是无比的契合。

    所以我们直接用BST查就可以,任何平衡树算法都是一样的。

    代码:

    int pre(int x)//前驱
    {
        int ans;
        int pos=root;
        while(pos)
        {
            if(x>val[pos])
            {
                ans=val[pos];
                pos=ch[pos][1];
            }
            else
                pos=ch[pos][0];
        }
        return ans;
    }
    int nxt(int x)//后继
    {
        int ans;
        int pos=root;
        while(pos)
        {
            if(x<val[pos])
            {
                ans=val[pos];
                pos=ch[pos][0];
            }
            else
                pos=ch[pos][1];
        }
        return ans;
    }
    
  • 相关阅读:
    Symfony Component HttpKernel Exception AccessDeniedHttpException This action is unauthorized.
    AngularJs ng-repeat中使用ng-model
    JS数组排序sort()方法同时按照两种方式排序的用法
    MongoDB
    Node基本学习
    小程序 五 组件使用
    小程序 四 事件类型
    小程序 二 template模板(代码复用)
    小程序 wxs 脚本语言(2种使用方式)
    小程序初体验 wx:for
  • 原文地址:https://www.cnblogs.com/fusiwei/p/13391634.html
Copyright © 2011-2022 走看看