草庐IT

2023.3.10 【模板】普通平衡树

fanghaoyu801212 2023-03-28 原文

2023.3.10 【模板】普通平衡树

推荐一篇写平衡树写的很好的博客:算法学习笔记(18): 平衡树(一) - jeefy - 博客园 (cnblogs.com)

问题陈述

写一种数据结构,支持以下六种操作:
1.插入一个数x

2.删除一个数x

3.查询x的排名(比x小的数 + 1)

4.查询排名为x的数

5.查询x的前驱

6.查询x的后继

这种操作可以用一个叫二叉查找树(BST)的东西实现,这玩意有以下性质:

\[subtree(lson(x)) < x < subtree(rson(x)) \]

翻译过来,就是一个节点左子树的值小于这个节点,右子树的值大于这个节点

这样在理想状态下,就可以每次从树根开始,实现这个问题,设操作数为Q,则理论时间复杂度为\(O(Qlogn)\)

但是会有(奇葩的)数据,使得构造的树长成这个样子:

这样深度变成了\(O(n)\),时间复杂度就变成了\(O(n^2)\),无法满足题目要求。

基于这一点,我们可以考虑将这个不平衡的树变成平衡的(大体平衡的就行)。

总体上考虑随机算法。

以下将呈现三种平衡树:

Treap

Treap,意为Tree + Heap,字面理解,就是这棵树既具有堆的性质,又具有BST的性质。

如何做到呢?我们在每个节点上给它赋一个随机值Heap,依照Heap值的性质来进行操作。

我们可以发现,上面那棵树其实有好几种形态,而且都具有BST的性质:


可以观察到,这棵树像是一根悬挂在螺丝钉上的链子一样,可以往左拉,也可以往右拉。我们称之为”旋转“操作,这是平衡树最重要的操作之一。而”旋转“操作又分为”左旋“和”右旋“。

”左旋“ (Zag)

左旋,即”向左旋转“,即向左拉这条链子,具体些,我们使用一张图:

观察上图,我们发现有三个点的父、子关系发生了变化:1(grandfa)、3(fa)、4(grandson左) 和 5(son),(以下简称为gf,f,s,gs)

按照如下顺序改变关系:

1.f的右儿子 = gs

2.gs == 0 ? NULL : gs的父亲 = f

3.s的左儿子 = f

4.f的父亲 = s

5.gf == 0 ? 根 = s : gf相应方向的儿子 = s

6.s的父亲 = gf

改变后不要忘记维护值

至于那个”相应方向的儿子“,可以用一个get()函数来维护

inline int get(int x)//0左1右 
{
    return x == t[t[x].fa].rc;
}
t[f].rc = t[s].lc;
t[t[s].lc].fa = f;
t[s].lc = f;
if(gf != 0)
    (get(f) ? t[gf].rc : t[gf].lc) = s;
else
    root = s;
t[s].fa = gf;
t[f].fa = s;
maintain(x);
maintain(y);
if(z)
    maintain(z);

右旋(Zig)

同理,只是将方向改变一下即可:(以下gs为grandson右)

1.f的左儿子 = gs

2.gs == 0 ? NULL : gs的父亲 = f

3.s的右儿子 = f

4.f的父亲 = s

5.gf == 0 ? 根 = s : gf相应方向的儿子 = s

6.s的父亲 = gf

t[f].lc = t[s].rc;
t[t[s].rc].fa = f;
t[s].rc= f;
if(gf != 0)
    (get(f) ? t[gf].rc : t[gf].lc) = s;
else
    root = s;
t[s].fa = gf;
t[f].fa = s;
maintain(x);
maintain(y);
if(z)
    maintain(z);

为了方便,以上两个函数可以合并成一个函数rorate()

插入

首先按照BST的性质造点,在判断如果子节点的Heap值小于父节点的Heap值,就旋转子节点

删除

将这个点的两个儿子中Heap值小的旋转上来,直到这个点成为叶节点,删除这个点(直接断开它的father与它的联系即可)。注意删除后要沿着父亲这条链一路向上更新。

查找x的排名

直接找到比这个数小的数,然后+1即可

inline int Get_Rank_By_Val(int k,int x)
{
    if(x == 0) return 1;
    if(t[x].val == k) return t[t[x].lc].siz + 1;
    if(t[x].val < k) return t[x].cnt + t[t[x].lc].siz + Get_Rank_By_Val(k,t[x].rc);
    if(t[x].val > k) return Get_Rank_By_Val(k,t[x].lc);    
}

查找排名为x的数

如果当前排名在左子树大小 + 1 和 左子树大小 + 当前节点重复数 之间,就返回当前排名

inline int Get_Val_By_Rank(int k,int x)
{
    if(t[t[x].lc].siz + 1 <= k && k <= t[t[x].lc].siz + t[x].cnt) return t[x].val;
    if(k <= t[t[x].lc].siz)
        return Get_Val_By_Rank(k,t[x].lc);
    else
        return Get_Val_By_Rank(k - t[t[x].lc].siz - t[x].cnt,t[x].rc);
}

查找前驱

如果当前数大于等于k,就向左子树寻找,否则就记录下答案,然后找右子树即可

inline int Getpre(int k,int x)
{
    if(x == 0) return -0x7f7f7f7f;
    if(t[x].val >= k)
        return Getpre(k,t[x].lc);
    int maxi = -0x7f7f7f7f;
    if(t[x].val < k)
        maxi = max(maxi,Getpre(k,t[x].rc));
    maxi = max(maxi,t[x].val);
    return maxi;
}

查找后继

同理

inline int Getpost(int k,int x)
{
    if(x == 0) return 0x7f7f7f7f;
    if(t[x].val <= k)
        return Getpost(k,t[x].rc);
    int mini = 0x7f7f7f7f;
    if(t[x].val > k)
        mini = min(mini,Getpost(k,t[x].lc));
    mini = min(mini,t[x].val);
    return mini;
}

这样,我们就完成了Treap的编写。

Splay

Splay树最有特色的就是它的Splay函数,这个函数的作用是将当前节点一直转到根。通过将很多节点随机旋转到根的做法保持平衡,删除的时候还是将它旋转到叶节点

Splay完整代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct Splay_Tree{
    int lc,rc,siz,fa,val,cnt;
}t[N];
int n,root,tot = 0;
inline int maintain(int x)
{
    t[x].siz = t[t[x].lc].siz + t[t[x].rc].siz + t[x].cnt;
}
inline int get(int x)//0左1右 
{
    return x == t[t[x].fa].rc;
}
inline void rorate(int x)
{
    int y = t[x].fa,z = t[y].fa;
    if(!get(x))
    {
        t[y].lc = t[x].rc;
        t[t[x].rc].fa = y;
        t[x].rc = y;
        if(z != 0)
            (get(y) ? t[z].rc : t[z].lc) = x;
        else
            root = x;
        t[x].fa = z;
        t[y].fa = x;
    }
    else
    {
        t[y].rc = t[x].lc;
        t[t[x].lc].fa = y;
        t[x].lc = y;
        if(z != 0)
            (get(y) ? t[z].rc : t[z].lc) = x;
        else
            root = x;
        t[x].fa = z;
        t[y].fa = x;
    }
    maintain(y);
    maintain(x);
    if(z != 0)
        maintain(z); 
}
inline void splay(int x)
{
    while(t[x].fa != 0)
    {
        if(t[x].fa == root)
            rorate(x);
        else if(get(x) == get(t[x].fa)) 
            rorate(t[x].fa),rorate(x);
        else
            rorate(x),rorate(x);
    }
    root = x;
}
inline void upd_line(int x)
{
    maintain(x);
    if(t[x].fa != 0)
        upd_line(t[x].fa);
}
inline void insert(int k,int x)
{
    if(t[x].val == 0)
    {
        if(root == 0)
        {
            root = ++tot;
            x = root;
        }
        t[x].cnt++;
        t[x].val = k;
        upd_line(x);
        splay(x);
        return;
    }
    if(t[x].val == k)
    {
        ++t[x].cnt;
        upd_line(x);
        splay(x);
        return;
    }
    if(k < t[x].val) 
    {
        if(t[x].lc == 0)
        {
            t[x].lc = ++tot;
            t[t[x].lc].fa = x;
        }
        insert(k,t[x].lc);
    }
    else
    {
        if(t[x].rc == 0) 
        {
            t[x].rc = ++tot;
            t[t[x].rc].fa = x;
        }
        insert(k,t[x].rc);
    }
    maintain(x);
}
inline int Get_Num_By_Val(int k,int x)
{
    if(k == t[x].val) return x;
    if(k < t[x].val)
    {
        if(t[x].lc != 0)
             return Get_Num_By_Val(k,t[x].lc);
         else
             return x;
    }
    else 
    {
        if(t[x].rc != 0)
            return Get_Num_By_Val(k,t[x].rc);
        else
            return x;
    }
}
inline int Get_Rank_By_Val(int k,int x)
{
    if(x == 0) return 1;
    if(t[x].val == k) return t[t[x].lc].siz + 1;
    if(t[x].val < k) return t[x].cnt + t[t[x].lc].siz + Get_Rank_By_Val(k,t[x].rc);
    if(t[x].val > k) return Get_Rank_By_Val(k,t[x].lc);    
}
inline int Get_Val_By_Rank(int k,int x)
{
    if(t[t[x].lc].siz + 1 <= k && k <= t[t[x].lc].siz + t[x].cnt) return t[x].val;
    if(k <= t[t[x].lc].siz)
        return Get_Val_By_Rank(k,t[x].lc);
    else
        return Get_Val_By_Rank(k - t[t[x].lc].siz - t[x].cnt,t[x].rc);
}
inline int Getpre(int k,int x)
{
    if(x == 0) return -0x7f7f7f7f;
    if(t[x].val >= k)
        return Getpre(k,t[x].lc);
    int maxi = -0x7f7f7f7f;
    if(t[x].val < k)
        maxi = max(maxi,Getpre(k,t[x].rc));
    maxi = max(maxi,t[x].val);
    return maxi;
}
inline int Getpost(int k,int x)
{
    if(x == 0) return 0x7f7f7f7f;
    if(t[x].val <= k)
        return Getpost(k,t[x].rc);
    int mini = 0x7f7f7f7f;
    if(t[x].val > k)
        mini = min(mini,Getpost(k,t[x].lc));
    mini = min(mini,t[x].val);
    return mini;
}
inline void del(int k,int x)
{
    x = Get_Num_By_Val(k,root);
    if(x == -1) return;
    if(t[x].cnt > 1)
    {
        --t[x].cnt;
        upd_line(x);
        splay(x);
        return;
    }
    while(t[x].lc != 0 || t[x].rc != 0)
    {
        if(t[x].lc != 0)
            rorate(t[x].lc);
        else
            rorate(t[x].rc);
    }
    if(t[x].fa != 0)
    {
        (get(x) ? t[t[x].fa].rc : t[t[x].fa].lc) = 0;
        upd_line(t[x].fa);
    }
    else
        root = 0;
}
int main()
{
    cin>>n;
    int op,x;
    for(int i = 1;i <= n;i++)
    {
        cin>>op>>x;
        switch(op)
        {
            case 1:
                insert(x,root);break;
            case 2:
                del(x,root);break;
            case 3:
                cout<<Get_Rank_By_Val(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
            case 4:
                cout<<Get_Val_By_Rank(x,root)<<endl;break;
            case 5:
                cout<<Getpre(x,root)<<endl;splay(Get_Num_By_Val(x,root));break;
            case 6:
                cout<<Getpost(x,root)<<endl;splay(Get_Num_By_Val(x,root));break; 
        }
    }    
}

乍一看,含有”旋转“的平衡树写起来都稍显麻烦,那么有什么方法能让我们不旋转也能实现平衡树呢?

FHQ-Treap

详见《范浩强谈数据结构》一书

FHQ-Treap,属于非旋Treap之一。有两个核心操作:分裂(split) 和 合并(merge)。一般所采取的分裂是按Tree值分裂,而合并是按Tree和Heap两个值合并。

Split

(采用OI-Wiki上面的图)

我们采用引用调用,在每走到一个点时,如果这个点小于等于key的值,我们就将这个节点归为左树,然后去分割它的右子树,反之亦然。这样到叶节点时就会将这棵树彻底剖开。

同时我们就不用将Tree值相同的节点放在一个点里面,而是分为多个点就好了。

inline void split(int &x,int &y,int now,int k)
{
    if(!now)
    {
        x = y = 0;
        return;
    }
    if(t[now].val <= k)
        x = now,split(t[now].rc,y,t[now].rc,k);
    else
        y = now,split(x,t[now].lc,t[now].lc,k);
    update(now);
}

Merge

分裂后的两棵树有一个极好的性质:左子树一定小于右子树。那么在合并的时候,我们就可以充分利用这个性质。我们利用x树小于y树的性质,所以要么x树和y树的左子树合并,要么y树和x树的右子树合并,至于选哪个,就按照Heap值判断即可。

inline int merge(int x,int y)//默认 tree[x] < tree[y] 
{
    if(!x || !y) return x + y;
    if(t[x].hp < t[y].hp) 
    {
        t[x].rc = merge(t[x].rc,y);
        update(x);
        return x;
    }
    else
    {
        t[y].lc = merge(x,t[y].lc);
        update(y);
        return y;
    }
}

Insert

将整棵树按照x值分裂,将小树与x合并,再与大树合并即可。

inline int new_node(int x)
{
    ++tot;
    t[tot].lc = t[tot].rc = 0;
    t[tot].val = x;
    t[tot].hp = rand();
    t[tot].siz = 1;
    return tot;
}
inline void insert(int x)
{
    int y,z;
    split(y,z,root,x);
    root = merge(merge(y,new_node(x)),z);
}

Delete

将整棵树按照x值分裂,将小树按照x - 1分裂,为了防止等于x值的树(简称x树),将x定义为x的左儿子和右儿子合并即可,在分别将x - 1树,x树,大树合并。

inline void del(int x)
{
    int y,z,w,p;
    split(y,z,root,x);
    split(w,p,y,x - 1);
    p = merge(t[p].lc,t[p].rc);
    root = merge(merge(w,p),z);
}

求x的排名

将整棵树按照x - 1分裂,然后求小树的大小 + 1即可

inline int Get_Rank_By_Val(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[y].siz + 1;
    root = merge(y,z);
    return ret;
}

求x的前驱

将整棵树按照x - 1分裂,然后取小树中的最大值即可

inline int Getpre(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[kth(t[y].siz,y)].val;
    root = merge(y,z);
    return ret;
}

求x的后继

按照x + 1分裂,然后求大树中的最小值即可

inline int Getpost(int x)
{
    int y,z;
    split(y,z,root,x);
    int ret = t[kth(1,z)].val;
    root = merge(y,z);
    return ret;
}

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct fhq_treap{
    int lc,rc,val,hp,siz;
}t[N];
int root = 0,tot = 0;
inline void update(int pos)
{
    t[pos].siz = t[t[pos].lc].siz + t[t[pos].rc].siz + 1;
}
inline void split(int &x,int &y,int now,int k)
{
    if(!now)
    {
        x = y = 0;
        return;
    }
    if(t[now].val <= k)
        x = now,split(t[now].rc,y,t[now].rc,k);
    else
        y = now,split(x,t[now].lc,t[now].lc,k);
    update(now);
}
inline int merge(int x,int y)//默认 tree[x] < tree[y] 
{
    if(!x || !y) return x + y;
    if(t[x].hp < t[y].hp) 
    {
        t[x].rc = merge(t[x].rc,y);
        update(x);
        return x;
    }
    else
    {
        t[y].lc = merge(x,t[y].lc);
        update(y);
        return y;
    }
}
inline int new_node(int x)
{
    ++tot;
    t[tot].lc = t[tot].rc = 0;
    t[tot].val = x;
    t[tot].hp = rand();
    t[tot].siz = 1;
    return tot;
}
inline void insert(int x)
{
    int y,z;
    split(y,z,root,x);
    root = merge(merge(y,new_node(x)),z);
}
inline void del(int x)
{
    int y,z,w,p;
    split(y,z,root,x);
    split(w,p,y,x - 1);
    p = merge(t[p].lc,t[p].rc);
    root = merge(merge(w,p),z);
}
inline int Get_Rank_By_Val(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[y].siz + 1;
    root = merge(y,z);
    return ret;
}
inline int kth(int x,int pos)
{
    if(x == t[t[pos].lc].siz + 1) return pos;
    else if (x > t[t[pos].lc].siz + 1) return kth(x - t[t[pos].lc].siz - 1,t[pos].rc);
    else return kth(x,t[pos].lc);
}
inline int Getpre(int x)
{
    int y,z;
    split(y,z,root,x - 1);
    int ret = t[kth(t[y].siz,y)].val;
    root = merge(y,z);
    return ret;
}
inline int Getpost(int x)
{
    int y,z;
    split(y,z,root,x);
    int ret = t[kth(1,z)].val;
    root = merge(y,z);
    return ret;
}
int main()
{
    srand(time(NULL));
    int n,opt,x;
    cin>>n;
    for(int i = 1;i <= n;i++)
    {
        cin>>opt>>x;
        switch(opt)
        {
            case 1:
                insert(x);break;
            case 2:
                del(x);break;
            case 3:
                cout<<Get_Rank_By_Val(x)<<endl;break;
            case 4:
                cout<<t[kth(x,root)].val<<endl;break;
            case 5:
                cout<<Getpre(x)<<endl;break;
            case 6:
                cout<<Getpost(x)<<endl;break;
        }
    }
    return 0;
}

完结撒花

有关2023.3.10 【模板】普通平衡树的更多相关文章

  1. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

  2. ruby - 匹配未转义的平衡定界符对 - 2

    如何匹配未被反斜杠转义的平衡定界符对(其本身未被反斜杠转义)(无需考虑嵌套)?例如对于反引号,我试过了,但是转义的反引号没有像转义那样工作。regex=/(?!$1:"how\\"#expected"how\\`are"上面的正则表达式不考虑由反斜杠转义并位于反引号前面的反斜杠,但我愿意考虑。StackOverflow如何做到这一点?这样做的目的并不复杂。我有文档文本,其中包括内联代码的反引号,就像StackOverflow一样,我想在HTML文件中显示它,内联代码用一些spanMaterial装饰。不会有嵌套,但转义反引号或转义反斜杠可能出现在任何地方。

  3. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  4. ruby-on-rails - Mandrill API 模板 - 2

    我正在使用Mandrill的RubyAPIGem并使用以下简单的测试模板:testastic按照Heroku指南中的示例,我有以下Ruby代码:require'mandrill'm=Mandrill::API.newrendered=m.templates.render'test-template',[{:header=>'someheadertext',:main_section=>'Themaincontentblock',:footer=>'asdf'}]mail(:to=>"JaysonLane",:subject=>"TestEmail")do|format|format.h

  5. ruby - Chef Ruby 遍历 .erb 模板文件中的属性 - 2

    所以这可能有点令人困惑,但请耐心等待。简而言之,我想遍历具有特定键值的所有属性,然后如果值不为空,则将它们插入到模板中。这是我的代码:属性:#===DefaultfileConfigurations#default['elasticsearch']['default']['ES_USER']=''default['elasticsearch']['default']['ES_GROUP']=''default['elasticsearch']['default']['ES_HEAP_SIZE']=''default['elasticsearch']['default']['MAX_OP

  6. 由于 libgmp.10.dylib 的问题,Ruby 2.2.0 无法运行 - 2

    我刚刚安装了带有RVM的Ruby2.2.0,并尝试使用它得到了这个:$rvmuse2.2.0--defaultUsing/Users/brandon/.rvm/gems/ruby-2.2.0dyld:Librarynotloaded:/usr/local/lib/libgmp.10.dylibReferencedfrom:/Users/brandon/.rvm/rubies/ruby-2.2.0/bin/rubyReason:Incompatiblelibraryversion:rubyrequiresversion13.0.0orlater,butlibgmp.10.dylibpro

  7. ruby - ri 有空文件 – Ubuntu 11.10, Ruby 1.9 - 2

    我正在运行Ubuntu11.10并像这样安装Ruby1.9:$sudoapt-getinstallruby1.9rubygems一切都运行良好,但ri似乎有空文档。ri告诉我文档是空的,我必须安装它们。我执行此操作是因为我读到它会有所帮助:$rdoc--all--ri现在,当我尝试打开任何文档时:$riArrayNothingknownaboutArray我搜索的其他所有内容都是一样的。 最佳答案 这个呢?apt-getinstallri1.8编辑或者试试这个:(非rvm)geminstallrdocrdoc-datardoc-da

  8. ruby-on-rails - gem install rmagick -v 2.13.1 错误 Failed to build gem native extension on Mac OS 10.9.1 - 2

    我已经通过提供MagickWand.h的路径尝试了一切,我安装了命令工具。谁能帮帮我?$geminstallrmagick-v2.13.1Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingrmagick:ERROR:Failedtobuildgemnativeextension./Users/ghazanfarali/.rvm/rubies/ruby-1.8.7-p357/bin/rubyextconf.rbcheckingforRubyversion>=1.8.5...yescheckingfor/

  9. ruby - 如何通过Middleman安装和使用Slim模板引擎 - 2

    一般来说,我是Middleman和ruby​​的新手。我已经安装了Ruby我已经安装了Middleman和gem以使其运行。我需要使用slim而不是默认的模板系统。所以我安装了Slimgem。Slim的网站只说我需要'slim'才能让它工作。中间人网站说我只需要在config.rb文件中添加模板引擎,但是没有给出例子...对于没有ruby​​背景的人来说,这没有帮助。我在git上找了几个config.rb,它们都有:require'slim'和#Setslim-langoutputstyleSlim::Engine.set_default_options:pretty=>true#Se

  10. ruby - 安装 tiny_tds 在 mac os 10.10.5 上出现错误 - 2

    我正在使用macos,我想使用ruby​​驱动程序连接到sqlserver。我想使用tiny_tds,但它给出了缺少free_tds的错误,但它已经安装了。怎么能过这个?~brewinstallfreetdsWarning:freetds-0.91.112alreadyinstalled~sudogeminstalltiny_tdsBuildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingtiny_tds:ERROR:Failedtobuildgemnativeextension.完整日志如下:/System

随机推荐