zoukankan      html  css  js  c++  java
  • Splay基本操作

    我们以一道题来引入吧!

    传送门

    题目说的很清楚,我们的数据结构要支持:插入x数,删除x数,查询数的排名和排名为x的数,求一个数前驱后继。

    似乎用啥现有的数据结构都很难做到在O(nlogn)的复杂度中把这些问题全部解决……(别跟我说什么set,vector……)

    所以我们来介绍一种新的数据结构——平衡树splay!

    什么是平衡树呢?这是一种数据结构,满足以下性质:

    1.它是一棵二叉树

    2.对于任意一个节点,它的左子树中的任意一个节点的权值比他小,右子树中任意一个节点的权值比他大

    3.一棵平衡树的任意一棵子树也是一个平衡树(其实第二条已经说明了这个事了)

    比如说这就是一棵平衡树(偷了yyb神犇的图),它现在非常优秀,深度是logn的。但是如果向一条链上插入多个数,或者出现什么奇怪的操作使平衡树退化为链,那么再进行查找等操作是很慢的,最坏甚至会到O(n)……比如下面这样(仍然偷了yyb神犇的图)

    平衡树本身是可以支持上面所说的这些查询操作的(因为自身本身优秀的性质使得其可以进行二分),但是为了解决他有可能退化为链的这么个问题,神犇tarjan发明了一种新的数据结构——splay。

    splay(伸展树),核心操作是rotate和splay(多个rotate),以使得对于每次操作之后树依然能保持一个良好的身材,使之不至于被退化为链,每次操作都能快速的在O(logn)中完成。这种操作使得每次改变之后这棵树仍然是一颗平衡树,但是节点的顺序发生了一些改变使得深度保持稳定。我们来看个图理解一下。

    (以下图均偷自yyb神犇)

     如图,x,y,z是三个节点,A,B,C是三棵子树。因为是一棵平衡树所以现在必然满足x<y<z.

    现在我们看似乎x所在的那一侧有点长……所以我们不妨尝试把x旋转到y的位置,这样就能重构这棵树的结构,或许能使这棵树的深度变得更优。

    首先我们肯定要保证新构成的树还是一棵平衡树,所以对于x,y,z三个节点,必然x,y还是在z的左子树中,既然把x移动到y的位置,那么x自然就成了z的左子树。不过平衡树还是一棵二叉树,那么y节点肯定不能呆在那了,它应该去哪呢?

    我们思考一下,首先因为必须满足y>x,y<z,而x又是z的左儿子,所以y必然在x的左右子树中。而y还比x大,所以y自然应该在x的右子树中。那我们就直接把y变成x的右儿子就好啦!不过这样的话,x,y的子树又应该怎么办呢?x原来是y的左儿子,现在x成了y的父亲,那么y的左儿子那个位置肯定是空的,然后又因为x现在右儿子是y,它原来的右儿子多了出来(就是上面图的B)。B必然保证所有节点都<y,y现在还缺左儿子,所以直接把B变为y的左子树即可。

    这样它就变成了这个样子,而且仍然是一棵平衡树。(上面已经说明过了,也可以自己看看验证一下)

     不过其实不只有这一种情况,x,y,z之间的关系一共有四种,随便推一推画一画就能看出来。

    但是我们不可能写四个函数的对吧!所以我们要找出其中的普遍性规律,使得我们写一个函数就能解决问题。

    首先因为肯定是用x去代替y,那么y是z的哪个儿子,转化之后x也必然是z的哪个儿子。

    之后,我们又发现,一开始x是y的哪个儿子,那么在进行一次转变之后x的那个儿子就不会变化。为什么呢?假如说现在x是y的右儿子,那么必然满足x>y.而经过一次转变之后,x成为了z的一个儿子,那么y只能成为x的其中一个儿子(这个上文已经提到过了),那么现在y<x,y自然就成为了x的左儿子,相对应的x左儿子移动,右儿子并没有变化。反过来也是一个道理。

    简单的来说就一句话:x是y的哪个儿子,那么相对应的x的那个儿子一定比其父亲(x)在权值方面(大/小)更优,所以y必然不会更新到它。

    最后就很容易看出来了,x是y的哪个儿子,y就会成为x与之相对应的儿子(比如x是y的左儿子,y就会成为x的右儿子)

    总结一下:(直接抄yyb神犇的话了,比较简洁)

    所以总结一下: 
    1.X变到原来Y的位置 
    2.Y变成了 X原来在Y的 相对的那个儿子 
    3.Y的非X的儿子不变 X的 X原来在Y的 那个儿子不变 
    4.X的 X原来在Y的 相对的 那个儿子 变成了 Y原来是X的那个儿子

    我们上一份代码来看一下吧!

    bool get(int x)//get用于返回当前节点是自己父亲的哪个儿子
    {
        return t[t[x].fa].ch[1] == x;
    }
    
    void pushup(int x)
    {
        t[x].son = t[t[x].ch[0]].son + t[t[x].ch[1]].son + t[x].cnt;
    }//更新自己为根的子树大小,其中cnt是这个数出现的次数
    
    void rotate(int x)//旋转操作
    {
        int y = t[x].fa,z = t[y].fa,k = get(x);
        t[z].ch[t[z].ch[1] == y] = x,t[x].fa = z;//更改z节点的儿子,x节点的父亲
        t[y].ch[k] = t[x].ch[k^1],t[t[y].ch[k]].fa = y;//更改y节点的一个儿子和它的父亲
        t[x].ch[k^1] = y,t[y].fa = x;//更改x节点的儿子和y节点的父亲
        pushup(x),pushup(y);//更新状态
    }

    是不是炒鸡好理解呀QWQ

    那么splay最核心的操作我们就说完了。下面我们再来说说splay操作!(要不这玩意为啥叫splay啊……)

    其实splay操作就是rotate操作的叠加,本来rotate操作是很优秀的一种操作,不过实际上还会发生一些奇怪的事情,我们要考虑这样一些问题。

    比如说我们看这样一棵平衡树。

    好的,如果你直接两次右rotate把x旋到z的位置,那么你将得到这样一棵平衡树:

    我们仔细观察之后发现一个神奇的问题,有一条链其实没有发生长度变化。就是z-y-x-b这条链,两次旋转之后变成了z-x-y-b,但是本身的长度是没有改变的。

    这样splay就容易被卡,比如你一直往这条链里面插入元素,它可能就变得很长很长很长,你查询的速度又会很慢很慢很慢。

    那咋办?难道伟大的splay就不能实现了嘛?当然不是,我们只要更改一下rotate的办法即可。对于上面这种状况,我们可以自己手画一下,如果先把y旋转到z的位置,再把x旋转到y的位置,这条链的长度就缩短了。

    这个也是有普遍性结论的!我们发现如果x,y是y,z的同一个儿子,那么就先旋转y再旋转x,否则直接旋转两次x即可。(这段大家手画一下理解一下更好)

    可能有人会提出这样的疑问,就是在更改了rotate顺序之后,当前这条链确实缩短了,但是相对的还有一(多)条链可能没有长度变化,这个不会被卡吗?

    仔细想之后是不会的,因为我们每次splay操作是从当前被修改的节点开始的。所以只要保证当前所在链长度不会增大即OK。况且从长远来看,你构造出一个splay之后,它本身是会接近满二叉树的一种状态,你对其进行一次splay,它仍然是接近满二叉树的状态,故进过多次splay之后,这棵splay依然是接近满二叉树的良好身材。

    我们来看一下splay操作的代码!

    void splay(int x,int goal)//goal是要旋转到的目标节点
    {
        while(t[x].fa != goal)
        {
        int y = t[x].fa,z = t[y].fa;
        if(z != goal) (t[y].ch[0] == x) ^ (t[z].ch[0] == y) ? rotate(x) : rotate(y);//对于左右儿子的讨论
        rotate(x);
        }
        if(goal == 0) root = x;//更改根节点
    }

    ovo其实到这里splay最核心的俩操作已经结束了。那我们来继续说一说怎么支持上面的这些操作。

    首先是查找find操作,查找一个数的位置。对于一个节点,左面全比他小,右面全比他大,那我们就可以直接愉快的二分,向左/右递归即可。然后我们把这个节点splay到根,方便接下来肆意(划死)操作。

    我们直接看代码吧!

    void find(int x)
    {
        int u = root;
        if(!u) return;
        while(t[u].ch[x > t[u].val] && x != t[u].val) u = t[u].ch[x > t[u].val];//通过x和当前节点值的大小比较来确定应该向哪里递归
        splay(u,0);
    }

    然后是insert操作,插入一个数。我们首先找到这个数在平衡树中哪个位置,如果这个节点已经存在的话,那我们把这个节点出现次数++,否则直接新建一个节点。

    void insert(int x)
    {
        int u = root,f = 0;
        while(u && t[u].val != x) f = u,u = t[u].ch[x > t[u].val];
        if(u) t[u].cnt++;
        else
        {
        u = ++idx;
        if(f) t[f].ch[x > t[f].val] = u;
        t[u].ch[0] = 0,t[u].ch[1] = 0;
        t[u].fa = f,t[u].val = x,t[u].cnt = 1,t[u].son = 1;
        }
        splay(u,0);
    }

    在之后,前驱/后继操作(通过传0/1可以实现用一个函数同时找前驱/后继)。一个数的前驱就是其左子树中最靠右的节点,后继同理。

    int next(int x,int f)
    {
        find(x);
        int u = root;
        if((t[u].val > x && f) || (t[u].val < x && !f)) return u;//如果当前节点权值大于x而且要找后继,或者当前节点权值小于x同时要找前驱
        u = t[u].ch[f];
        while(t[u].ch[f^1]) u = t[u].ch[f^1];//要往反方向跳
        return u;
    }

    当然我们可以选择分开写(这个是求根的前驱后继,实际使用的时候可以转化为先求一个点的编号再这样做)

    int pre()
    {
        int u = t[root].ch[0];
        while(t[u].ch[1]) u = t[u].ch[1];
        return u;
    }
    int next
    {
      int u = t[root].ch[1];
        while(t[u].ch[0]) u = t[u].ch[0];
        return u;
    }

    最后是删除del操作。这个还是稍微有点麻烦。我们首先找到x的前驱,把它旋转到根,再找到x的后继把它旋转到当前的根(x的前驱)的右子树,那么这个时候x的后继的左子树里面就只可能有x,把它删了。如果出现次数大于1就cnt--并且splay到根,否则直接删了完事。

    void del(int x)
    {
        int la = next(x,0),ne = next(x,1);
        splay(la,0),splay(ne,la);
        int g = t[ne].ch[0];
        if(t[g].cnt > 1) t[g].cnt--,splay(g,0);
        else t[ne].ch[0] = 0;
    }

    哎不对,还有一个排名为x的数。这个也比较容易,我们还是用二分的思想即可。

    int rk(int x)
    {
        int u = root;
        if(t[u].son < x) return 0;
        while(1)
        {
        int y = t[u].ch[0];
        if(x > t[y].son + t[u].cnt) x -= (t[y].son + t[u].cnt),u = t[u].ch[1];//这时要向右子树找
        else if(t[y].son >= x) u = y;向左子树找
        else return t[u].val;//否则说明找到了,返回
        }
    }

    那我们就成功的用splay解决了这些问题!我们来看一下完整代码。

    // luogu-judger-enable-o2
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<iostream>
    #include<cmath>
    #include<queue>
    #include<set>
    #define rep(i,a,n) for(int i = a;i <= n;i++)
    #define per(i,n,a) for(int i = n;i >= a;i--)
    #define enter putchar('
    ')
    
    using namespace std;
    typedef long long ll;
    const int M = 100005;
    const int INF = 2147483647;
    
    int read()
    {
        int ans = 0,op = 1;
        char ch = getchar();
        while(ch < '0' || ch > '9')
        {
        if(ch == '-') op = -1;
        ch = getchar();
        }
        while(ch >= '0' && ch <= '9')
        {
        ans *= 10;
        ans += ch - '0';
        ch = getchar();
        }
        return ans * op;
    }
    
    struct node
    {
        int fa,son,ch[2],cnt,val;
    }t[M<<2];
    
    int n,root = 0,idx,op,x;
    
    void pushup(int x)
    {
        t[x].son = t[t[x].ch[0]].son + t[t[x].ch[1]].son + t[x].cnt;
    }
    
    bool get(int x)
    {
        return t[t[x].fa].ch[1] == x;
    }
    
    void rotate(int x)
    {
        int y = t[x].fa,z = t[y].fa,k = get(x);
        t[z].ch[t[z].ch[1] == y] = x,t[x].fa = z;
        t[y].ch[k] = t[x].ch[k^1],t[t[y].ch[k]].fa = y;
        t[x].ch[k^1] = y,t[y].fa = x;
        pushup(x),pushup(y);
    }
    
    void splay(int x,int goal)
    {
        while(t[x].fa != goal)
        {
        int y = t[x].fa,z = t[y].fa;
        if(z != goal) (t[y].ch[0] == x) ^ (t[z].ch[0] == y) ? rotate(x) : rotate(y);
        rotate(x);
        }
        if(goal == 0) root = x;
    }
    
    void insert(int x)
    {
        int u = root,f = 0;
        while(u && t[u].val != x) f = u,u = t[u].ch[x > t[u].val];
        if(u) t[u].cnt++;
        else
        {
        u = ++idx;
        if(f) t[f].ch[x > t[f].val] = u;
        t[u].ch[0] = 0,t[u].ch[1] = 0;
        t[u].fa = f,t[u].val = x,t[u].cnt = 1,t[u].son = 1;
        }
        splay(u,0);
    }
    
    void find(int x)
    {
        int u = root;
        if(!u) return;
        while(t[u].ch[x > t[u].val] && x != t[u].val) u = t[u].ch[x > t[u].val];
        splay(u,0);
    }
    
    int next(int x,int f)
    {
        find(x);
        int u = root;
        if((t[u].val > x && f) || (t[u].val < x && !f)) return u;
        u = t[u].ch[f];
        while(t[u].ch[f^1]) u = t[u].ch[f^1];
        return u;
    }
    
    void del(int x)
    {
        int la = next(x,0),ne = next(x,1);
        splay(la,0),splay(ne,la);
        int g = t[ne].ch[0];
        if(t[g].cnt > 1) t[g].cnt--,splay(g,0);
        else t[ne].ch[0] = 0;
    }
    
    int rk(int x)
    {
        int u = root;
        if(t[u].son < x) return 0;
        while(1)
        {
        int y = t[u].ch[0];
        if(x > t[y].son + t[u].cnt) x -= (t[y].son + t[u].cnt),u = t[u].ch[1];
        else if(t[y].son >= x) u = y;
        else return t[u].val;
        }
    }
    
    int main()
    {
        insert(INF),insert(-INF);
        n = read();
        rep(i,1,n)
        {
        op = read();
        if(op == 1) x = read(),insert(x);
        else if(op == 2) x = read(),del(x);
        else if(op == 3) x = read(),find(x),printf("%d
    ",t[t[root].ch[0]].son);
        else if(op == 4) x = read(),printf("%d
    ",rk(x+1));
        else if(op == 5) x = read(),printf("%d
    ",t[next(x,0)].val);
        else if(op == 6) x = read(),printf("%d
    ",t[next(x,1)].val);
        }
        return 0;
    }

    一开始插入INF和-INF的操作其实可以暂时忽略,那个是保证splay翻转区间时的正确性,还有好多其他实际题中的应用正确性的。(区间反转下次再说)

    splay是不是很好理解呢?(不过代码讲真还是比较难记……)

  • 相关阅读:
    WINDOWS SERVER 2008 RD服务器搭建
    EXCEL技巧——SUBTOTAL函数巧妙应用
    快速理解几种常用的RAID磁盘阵列级别
    有道云笔记去除左下角广告
    git教程
    .Net导出pdf文件,C#实现pdf导出
    时间控件只显示年月
    C#中日期和时间相加的方法
    JS获取当前时间
    六大设计原则
  • 原文地址:https://www.cnblogs.com/captain1/p/9733588.html
Copyright © 2011-2022 走看看