• 平衡树 替罪羊树(Scapegoat Tree)


    替罪羊树(Scapegoat Tree)

    入门模板题 洛谷oj P3369

    题目

      您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

    1. 插入xx数
    2. 删除xx数(若有多个相同的数,因只删除一个)
    3. 查询xx数的排名(排名定义为比当前数小的数的个数+1+1。若有多个相同的数,因输出最小的排名)
    4. 查询排名为xx的数
    5. xx的前驱(前驱定义为小于xx,且最大的数)
    6. xx的后继(后继定义为大于xx,且最小的数)

    输入格式

      第一行为n,表示操作的个数,下面n行每行有两个数optxopt表示操作的序号( 1opt6 )

    输出格式

      对于操作3,4,5,6每行输出一个数,表示对应答案

    输入样例

    10

    1 106465
    4 1
    1 317721
    1 460929
    1 644985
    1 84185
    1 89851
    6 81968
    1 492737
    5 493598

    输出样例

    106465

    84185
    492737

    数据范围

    1.n的数据范围: n100000

    2.每个数的数据范围: [-10^7, 10^7]

     

      网上的资料比较琐碎难懂,之前看了很多资料一直不能理解平衡树(我太弱了)……前几天突然莫名其妙明白了,想写一篇笔记记录一下(乱写一通)。

     

    0x00 二叉查找树

      要初步弄懂平衡树,首先要知道这是一棵二叉查找树

      二叉查找树(Binary Search Tree),当然也可以叫它二叉搜索树,或者二叉排序树(反正都一个意思都是二叉树),它的定义如下:

        或者是一棵空树,或者是具有下列性质的二叉树:

    (1)若左子树不空,则左子树上所有结点的值均小于它的根节点的值;
    (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
    (3)左、右子树也分别为二叉排序树;

        差不多就像下面这幅图一样:

    0x01 平衡树的用途

      在学平衡树这个数据结构前,相信我们一定会先有个问题:平衡树能拿来干什么?

      网上的很多资料对这一点写得不太明白(也可能是我太弱了),我先试着乱总结一下:
        现在需要一种数据结构,它需要做到以下几点:
         1. 高效地查询一个序列中某个数的前面和后面的数(a[i-1]和a[i+1])。
         2. 高效地知道第i个数是什么(即a[i])。
         3. 高效地插入和删除。
      显然,我们可以用普通数组优秀地完成第1点和第2点,但第3点不能够了。当然,我们也可以用链表,复杂度O(1)优秀地完成第1点和第3点,但是对于第2点,链表的复杂度就达到了不太理想的O(n)。
      那我们能不能想办法优化一下链表呢?


      我们可以回头看一下二叉搜索树的定义,然后我们会发现,假设存在一个从1到8的链表(就像下图,win自带画图画的,不太好看)

      其实它也满足左边小于右边的定义,也可以勉强算是一棵以序号为每个节点的权值的二叉查找树。

      

      但是这样的一棵二叉树是不是很难看很畸形?
      我们怎么想办法把它变成一棵比较好看的树呢?
      可以拿笔画画看:
      我们尝试把中间的节点(4或者5,我选择4)拎出来,然后就变成了这样:

      

      是不是好看了一点?有点树的形状了,那我们可以尝试继续把两侧链上的中间节点继续拎出来,不断重复,最终会变成一棵比较好看的树。

      
      这就是平衡二叉树,严格遵守了二叉查找树的定义——左儿子小于右儿子。
      也许你会有疑问:长成这样的一棵树,怎么做到刚刚链表都不能完全做到的3点要求呢?
      让我们再看看这棵树:

    0x02 查询前驱和后驱
      对于一个我们已知的节点
    i,我们先定义与它深度相同的都是它的兄弟节点。

      那么很显然,i的左兄弟及其子树上的所有节点都比i的左儿子及其子树上的所有节点来得小,且i的左儿子及其子树上的所有点都比i的父亲更大,所以显然,i的前驱要在i的左儿子及其子树上找。

      同理,仔细观察图,会发现我们所要找的前驱,存在于i的左儿子子树的最右侧,就是i的左儿子的右儿子的右儿子的右儿子的右儿子……(直到最后一个右儿子)

      这样,我们就可以O(log n)地求出i的前驱了。i的后驱同理。

    0x03 查询第i个数

      前面说过,我们用数的序列编号作为节点的排序权值,所以我们只要像线段树那样从根部开始查一遍就可以了。详细解释:

      从根结点开始,如果第i个数比当前节点j的序号小,就往左儿子搜,反之右儿子。直到找到第i个数,时间复杂度还是O(log n)。


    0x04 插入和删除
      插在原序列的末尾,所以新节点的编号是n+1。然后我们把这个节点变成根结点的右儿子的右儿子的右儿子……(变成最后一个没有右儿子的节点的右儿子)。

      删除。其实就是把i节点打个被删除掉的标记(甚至不打也可以)。然后让i的左右儿子之一成为i的父节点的新儿子(就是让某一个儿子取代i节点)。复杂度同O(log n)

      基本操作差不多就是这样。

      那么其实还有一个问题:如果操作太多,导致一棵本来平衡的树变成了一条链表,复杂度爆炸,怎么办呢?

    0x05 重新建树
      如果你选择的是替罪羊树,那就是优雅的暴力了。替罪羊树在每个节点上记录子树的节点数size,同时还有一个平衡因子alpha(通常在0.5左右,我选择0.7),当每次更新后,递归回去检查i节点的左右儿子分别乘以平衡因子,是否大于另一个儿子,如果大了,代表这棵树有退化的倾向,赶紧拍平重建(就是把树压成链表,重新建树)。

      大概就是这样,手机打的好难受,直接上代码好了。

      (其实我一开始做平衡树时觉得可以用线段树模拟的emmm,就不说了)

      代码可以配合洛谷的模板题食用

    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int INF=1000000000;
    const int MAX_N=2000005;
    const double alpha=0.75;
    
    int n;
    inline int read(){
        register int ch=getchar(),x=0,f=1;
        while (ch<'0'||ch>'9'){
            if (ch=='-')    f=-1;
            ch=getchar();
        } while (ch>='0'&&ch<='9'){
            x=x*10+ch-'0';
            ch=getchar();
        } return x*f;
    }
    struct Tree{
        int fa;
        int size;
        int value;
        int son[2];
    }tree[MAX_N];
    int cnt=2;
    int root=1;
    int node[MAX_N];
    int sum;
    
    bool balance(int x){    //判断是否平衡 
        return (double)tree[x].size*alpha>=(double)tree[tree[x].son[0]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[1]].size;
    }
    int build(int l,int r){    //重新递归建树 
        if (l>r)    return 0;
        int mid=(l+r)>>1;
        tree[tree[node[mid]].son[0]=build(l,mid-1)].fa=node[mid],tree[tree[node[mid]].son[1]=build(mid+1,r)].fa=node[mid];
        tree[node[mid]].size=tree[tree[node[mid]].son[0]].size+tree[tree[node[mid]].son[1]].size+1;
        return node[mid];
    }
    void recycle(int x){    //把树压成数列 
        if (tree[x].son[0])    recycle(tree[x].son[0]);
        node[++sum]=x;
        if (tree[x].son[1])    recycle(tree[x].son[1]);
    }
    void rebuild(int x){
        sum=0;
        recycle(x);
        int fa=tree[x].fa,son=(tree[tree[x].fa].son[1]==x),now=build(1,sum);
        tree[tree[fa].son[son]=now].fa=fa;
        if (x==root)    root=now;
    }
    void insert(int x){
        int i=root,now=++cnt;    //新节点序号 
        tree[now].size=1,tree[now].value=x;
        while (true){
            tree[i].size++;
            bool son=(x>=tree[i].value); 
            if (tree[i].son[son])    i=tree[i].son[son];
            else{
                tree[tree[i].son[son]=now].fa=i;
                break;
            }
        }
        int flag=0;
        for (int j=now;j;j=tree[j].fa)    //logn找不平衡的节点 
            if (!balance(j))    flag=j;
        if (flag)    rebuild(flag);    //重建树 
    }
    
    
    int get_num(int x){     
        int i=root;
        while (true){
            if(tree[i].value==x)    return i;
            else    i=tree[i].son[tree[i].value<x];
        }
    }
    void erase(int x){    //删除 
        if (tree[x].son[0]&&tree[x].son[1]){
            int now=tree[x].son[0];
            while (tree[now].son[1])    now=tree[now].son[1];
            tree[x].value=tree[now].value;
            x=now;
        }
        int son=(tree[x].son[0])?tree[x].son[0]:tree[x].son[1];
        int k=(tree[tree[x].fa].son[1]==x);
        tree[tree[tree[x].fa].son[k]=son].fa=tree[x].fa;
        for (int i=tree[x].fa;i;i=tree[i].fa)
            tree[i].size--;
        if (x==root)
            root=son;
    }
    int get_rank(int x){
        int i=root,ans=0;
        while (i)
            if(tree[i].value<x)    ans+=tree[tree[i].son[0]].size+1,i=tree[i].son[1];
            else    i=tree[i].son[0];
        return ans;
    }
    int get_kth(int x){
        int i=root;
        while (true)
            if (tree[tree[i].son[0]].size==x-1)    return i;
            else if (tree[tree[i].son[0]].size>=x)    i=tree[i].son[0];
            else    x-=tree[tree[i].son[0]].size+1,i=tree[i].son[1];
        return i;
    }
    int get_front(int x){
        int i=root,ans=-INF;
        while(i)
            if(tree[i].value<x)    ans=max(ans,tree[i].value),i=tree[i].son[1];
            else    i=tree[i].son[0];
        return ans;
    }
    int get_behind(int x){
        int i=root,ans=INF;
        while(i)
            if(tree[i].value>x)    ans=min(ans,tree[i].value),i=tree[i].son[0];
            else    i=tree[i].son[1];
        return ans;
    }
    int main(){
    //    freopen("test1.in","r",stdin);
        tree[1].value=-INF,tree[1].size=2,tree[1].son[1]=2;
        tree[2].value=INF,tree[2].size=1,tree[2].fa=1;
        n=read();
        for(int i=1,op,x;i<=n;i++){
            op=read(),x=read();
            if(op==1)    insert(x);
            if(op==2)    erase(get_num(x));
            if(op==3)    printf("%d
    ",get_rank(x));
            if(op==4)    printf("%d
    ",tree[get_kth(x+1)].value);
            if(op==5)    printf("%d
    ",get_front(x));
            if(op==6)    printf("%d
    ",get_behind(x));
        }
    }

    其他例题

    大体上按难度排序?我太弱了也搞不懂。

    一.[HNOI2002]营业额统计 (2019.4.10更新)

    洛谷oj P2234

      据说有各种神犇用许多神奇的解法A掉了……但是我这种蒟蒻就先用平衡树练手了。

      min{|该天以前某一天的营业额-该天营业额|},即不大于a[i]的最大值和不小于a[i]的最小值,就是寻找a[i]的前驱和后驱,分别减去a[i]取绝对值,再全部加起来,复杂度应该是O(nlogn)。

      代码:

    #include <cstdio>
    #include <iostream>
    using namespace std;
    const int INF=1000000000;
    const int MAX_N=2000005;
    const double alpha=0.75;
    
    int n;
    inline int read(){
        register int ch=getchar(),x=0,f=1;
        while (ch<'0'||ch>'9'){
            if (ch=='-')    f=-1;
            ch=getchar();
        } while (ch>='0'&&ch<='9'){
            x=x*10+ch-'0';
            ch=getchar();
        } return x*f;
    }
    struct Tree{
        int fa;
        int size;
        int value;
        int son[2];
    }tree[MAX_N];
    int cnt=2;
    int root=1;
    int node[MAX_N];
    int sum;
    
    bool balance(register int x){    //判断是否平衡 
        return (double)tree[x].size*alpha>=(double)tree[tree[x].son[0]].size&&(double)tree[x].size*alpha>=(double)tree[tree[x].son[1]].size;
    }
    inline int build(register int l,register int r){    //重新递归建树 
        if (l>r)    return 0;
        int mid=(l+r)>>1;
        tree[tree[node[mid]].son[0]=build(l,mid-1)].fa=node[mid],tree[tree[node[mid]].son[1]=build(mid+1,r)].fa=node[mid];
        tree[node[mid]].size=tree[tree[node[mid]].son[0]].size+tree[tree[node[mid]].son[1]].size+1;
        return node[mid];
    }
    void recycle(register int x){    //把树压成数列 
        if (tree[x].son[0])    recycle(tree[x].son[0]);
        node[++sum]=x;
        if (tree[x].son[1])    recycle(tree[x].son[1]);
    }
    void rebuild(register int x){
        sum=0;
        recycle(x);
        int fa=tree[x].fa,son=(tree[tree[x].fa].son[1]==x),now=build(1,sum);
        tree[tree[fa].son[son]=now].fa=fa;
        if (x==root)    root=now;
    }
    void insert(register int x){
        int i=root,now=++cnt;    //新节点序号 
        tree[now].size=1,tree[now].value=x;
        while (true){
            tree[i].size++;
            bool son=(x>=tree[i].value); 
            if (tree[i].son[son])    i=tree[i].son[son];
            else{
                tree[tree[i].son[son]=now].fa=i;
                break;
            }
        }
        int flag=0;
        for (int j=now;j;j=tree[j].fa)    //logn找不平衡的节点 
            if (!balance(j))    flag=j;
        if (flag)    rebuild(flag);    //重建树 
    }
    
    inline int get_front(register int x){
        register int i=root,ans=-INF;
        while(i)
            if(tree[i].value<x)    ans=max(ans,tree[i].value),i=tree[i].son[1];
            else    i=tree[i].son[0];
        return ans;
    }
    inline int get_behind(register int x){
        register int i=root,ans=INF;
        while(i)
            if(tree[i].value>x)    ans=min(ans,tree[i].value),i=tree[i].son[0];
            else    i=tree[i].son[1];
        return ans;
    }
    bool flag[1000000+5];
    int result;
    int main(){
    //  freopen("test1.in","r",stdin);
        tree[1].value=-INF,tree[1].size=2,tree[1].son[1]=2;
        tree[2].value=INF,tree[2].size=1,tree[2].fa=1;
        n=read();
        int kkk=read();
        result+=kkk;
        insert(kkk);
        flag[kkk]=true;
        for (int i=2,a,min,max;i<=n;i++){
            a=read();
            if (flag[a])    continue;
            flag[a]=true;
            insert(a);
            min=get_front(a);
            max=get_behind(a);
            if (min==-INF){
                result+=(max-a);
                continue;
            }
            else if (max==INF){
                result+=(a-min);
                continue;
            }
            result+=(a-min>max-a?max-a:a-min);
        }
        printf("%d",result);
        return 0;
    }

    [学习自百度百科和其他网络资料]

  • 相关阅读:
    MySQL主从复制的作用?
    MySQL的逻辑架构
    SQL语句的执行流程
    Count(*)在不同引擎的实现方式
    视图
    MySQL经典练习题(五)
    pyinstaller基本操作
    git基本操作
    Ubuntu安装tensorflow
    ScrollView can host only one direct child
  • 原文地址:https://www.cnblogs.com/awakening-orz/p/10660378.html
Copyright © 2020-2023  润新知