Loading

平衡树入门——FHQ_Treap

平衡树入门——FHQ_Treap

1 简介

FHQ treap,也有人称之为无旋 Treap,因为没有旋转,所以可以支持可持久化,也可以支持序列操作,常数略大,速度比 Splay 快。用两个操作——插入和删除就完成了对整个 Treap 的维护。自我感觉代码复杂度比 Treap,Splay 很低。容易理解,理解后记忆代码十分容易。

2 数据结构解析

2.1 节点信息

FHQ Treap 和 Treap 的节点信息是一样的——除了一点以外:

struct node{
    int val,size,key,l,r;
};
node p[N];

相信读者已经发现了,FHQ Treap 没有记录相同权值节点的个数,也就是说,就算有相同权值的节点,也不能够把这些节点当做一个节点来处理。这带来了一定意义上空间的浪费,不过少维护一些东西可以让编程复杂度降低。

这里 \(val\) 是权值,\(size\) 是子树大小,\(key\) 是 Treap 的随机值,\(l,r\) 分别是左右儿子。

2.2 创造新节点

    inline int new_node(int val){
        p[++tot].val=val;p[tot].key=random(INF);
        p[tot].l=p[tot].r=0;p[tot].size=1;
        return tot;
    }

其中:

inline int random(int n){
    return rand()*rand()%n+1;
}

这里 \(tot\) 是节点总量,\(val\) 是所插入节点的权值。这段代码不作讲解。

2.3 合并信息

    inline void pushup(int k){
        p[k].size=p[p[k].l].size+p[p[k].r].size+1;
    }

太过于简单,不作讲解。

2.4 分裂

inline void split(int k,int val,int &x,int &y){//depend on val
    if(k==0){
        x=y=0;return;
    }
    if(p[k].val<=val){
        x=k;
        split(p[k].r,val,p[k].r,y);
    }
    else{
        y=k;
        split(p[k].l,val,x,p[k].l);
    }
    pushup(k);
}

使用 split(k,val_,x,y) 就相当于完成了这样一件事情:把以 \(k\) 为根的子树按照权值分裂,其中权值小于等于 val_ 的最终会到以 \(x\) 为根的树中,大于 val_ 的权值会到以 \(y\) 为根的树中。注意到 \(x\)\(y\) 是通过引用传回来的。

我们现在重点关注一下他是如何分裂的,首先,如果 \(k=0\) 那么这就代表已经分裂结束或者这颗树为空,不管是哪一种情况,这个时候,让引用的 \(x,y\) 等于 \(0\) 是不影响结果的,因为这个时候并没有那一颗子树被分到了以 \(x\) 为根或以 \(y\) 为根的子树上去。

否则,如果这个节点的权值是小于等于 \(val\) 的,说明 \(k\) 这个节点和 \(k\) 这个节点的左子树都会被划分到 \(x\) 这颗子树上去,而此时此刻,\(k\) 这个节点的右子树还没有被划分,所以我们在去划分一下 \(k\) 的右子树,注意我们是带引用的去进行递归的,所以如果有要划分到 \(x\) 这个子树上的节点,我们就把它挂到右子树上去,这也是为什么我们把第三个参数变为 p[k].r 。第 \(9\) 到第 \(12\) 行同理。

这里的引用有效降低了编程复杂度。

当然我们依然可以使用大小进行分裂,通常来说,根据题目不同的要求,两种分裂方式都是必须要掌握的。整体思路和按照权值分裂一样。

inline void split(int k,int siz,int &x,int &y){
    if(!k){x=0;y=0;return;}
    if(p[p[k].l].size+1<=siz){
        x=k;split(p[k].r,siz-p[p[k].l].size-1,p[x].r,y);PushUp(x);
    }
    else{y=k;split(p[k].l,siz,x,p[y].l);PushUp(y);}
}

2.5 合并

    inline int merge(int x,int y){
        if(x==0||y==0) return x+y;
        if(p[x].key>p[y].key){
            p[x].r=merge(p[x].r,y);
            pushup(x);
            return x;
        }
        else{
            p[y].l=merge(x,p[y].l);
            pushup(y);
            return y;
        }
    }

这一段代码完成的是把以 \(x\) 为根的子树与以 \(y\) 为根的子树合并,注意这里保证以 \(x\) 为根的子树的权值最大值要小于以 \(y\) 为根的子树的权值最小值。注意这里我们需要维护优先级。因为有上面那个性质,所以我们不用判断节点权值大小而可以直接合并,最后这段代码传回的是合并完两颗子树后的根节点。

不难理解这段代码:如果 \(x\) 的优先值大于 \(y\) 的优先值,如图:

img

不难发现的是,\(x\) 的左子树就不需要进行合并的,需要在进行合并的是 \(x\) 的右子树和 \(y\) 这颗子树,递归进行就可以。\(y\) 的优先级更大是同理的。如果其中一颗子树为空,那么我们直接返回 \(x+y\) 就可以,如果另一颗子树不为空那么返回的就是另一颗子树的根节点,如果两颗子树都为空也不失正确性。

2.6 插入

其实有了上面这两个操作其它操作的实现就非常简单了。

    inline void insert(int val){
        int x,y;
        split(root,val-1,x,y);
        root=merge(merge(x,new_node(val)),y);
    }

这个函数能够往平衡树中插入一个权值为 \(val\) 的节点。

如何实现的呢?我们按照权值 \(val-1\) 来进行分裂,分裂之后,权值小于等于 \(val-1\) 的节点都在以 \(x\) 为根的子树中,其他节点在以 \(y\) 为根的子树中,然后我们先把 \(x\) 与我们新建的节点合并,然后再合并整棵树。

2.7 删除

    inline void delete_(int val){
        int x,y,z;
        split(root,val,x,z);
        split(x,val-1,x,y);
        if(y){
            y=merge(p[y].l,p[y].r);
        }
        root=merge(merge(x,y),z);
    }

不难发现在分裂之后,以 \(y\) 为根的子树里只有权值等于 \(val\) 的节点,我们合并左右子树——删除根就可以。

如果要删除所有权值为 \(val\) 的节点,就不用写第 \(6\) 到第 \(9\) 行。

删完之后,我们把整棵树重新合并。

2.8 查询排名

inline int getrank(int val){
    int x,y,ans;
    split(root,val-1,x,y);
    ans=p[x].size+1;
    root=merge(x,y);
    return ans;
}

某个数的排名就是比他小的数的个数 \(+1\) ,所以不难理解上面这个代码。

2.9 查询值

inline int getval(int rank){
    int k=root;
    while(k){
        if(p[p[k].l].size+1==rank) break;
        else if(p[p[k].l].size>=rank) k=p[k].l;
        else rank-=p[p[k].l].size+1,k=p[k].r;
    }
    return k==0?INF:p[k].val;
}

这个和普通的平衡树查询值是一样的,不作讲解。

2.10 查询前驱后继

inline int getpre(int val){
    int x,y,k,ans;
    split(root,val-1,x,y);
    k=x;
    while(p[k].r) k=p[k].r;
    ans=p[k].val;
    root=merge(x,y);
    return ans;
}
inline int getnext(int val){
    int x,y,k,ans;
    split(root,val,x,y);
    k=y;while(p[k].l) k=p[k].l;
    ans=p[k].val;
    root=merge(x,y);
    return ans;
}

如果要查询前驱,我们就按照 \(val-1\) 分裂整颗树,然后取 \(x\) 子树最靠右的节点就可以了,查询后继同理。

2.11 优化

因为在一定程度上 FHQ Treap 有点浪费空间,所以我们可以开一个栈,把所以删除的节点编号存一下,然后插入新节点时我们优先使用这些被删除的节点。代码就看这篇博客,同时这篇博客还讲解了如何判断某一个节点是否存在,以及返回整颗输的大小等操作,相信有了分裂和合并,这些操作不难实现。

3 总代码

#include<bits/stdc++.h>
#include<cstdlib>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 500100
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T>  inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct node{
    int val,size,key,l,r;
};
node p[N];

inline int random(int n){
    return rand()*rand()%n+1;
}

struct FHQ_Treap{
    int tot,root;
    inline void pushup(int k){
        p[k].size=p[p[k].l].size+p[p[k].r].size+1;
    }
    inline int new_node(int val){
        p[++tot].val=val;p[tot].key=random(INF);
        p[tot].l=p[tot].r=0;p[tot].size=1;
        return tot;
    }
    inline void split(int k,int val,int &x,int &y){//depend on val
        if(k==0){
            x=y=0;return;
        }
        if(p[k].val<=val){
            x=k;
            split(p[k].r,val,p[k].r,y);
        }
        else{
            y=k;
            split(p[k].l,val,x,p[k].l);
        }
        pushup(k);
    }
    inline int merge(int x,int y){
        if(x==0||y==0) return x+y;
        if(p[x].key>p[y].key){
            p[x].r=merge(p[x].r,y);
            pushup(x);
            return x;
        }
        else{
            p[y].l=merge(x,p[y].l);
            pushup(y);
            return y;
        }
    }
    inline void insert(int val){
        int x,y;
        split(root,val-1,x,y);
        root=merge(merge(x,new_node(val)),y);
    }
    inline void delete_(int val){
        int x,y,z;
        split(root,val,x,z);
        split(x,val-1,x,y);
        if(y){
            y=merge(p[y].l,p[y].r);
        }
        root=merge(merge(x,y),z);
    }
    inline int getrank(int val){
        int x,y,ans;
        split(root,val-1,x,y);
        ans=p[x].size+1;
        root=merge(x,y);
        return ans;
    }
    inline int getval(int rank){
        int k=root;
        while(k){
            if(p[p[k].l].size+1==rank) break;
            else if(p[p[k].l].size>=rank) k=p[k].l;
            else rank-=p[p[k].l].size+1,k=p[k].r;
        }
        return k==0?INF:p[k].val;
    }
    inline int getpre(int val){
        int x,y,k,ans;
        split(root,val-1,x,y);
        k=x;
        while(p[k].r) k=p[k].r;
        ans=p[k].val;
        root=merge(x,y);
        return ans;
    }
    inline int getnext(int val){
        int x,y,k,ans;
        split(root,val,x,y);
        k=y;while(p[k].l) k=p[k].l;
        ans=p[k].val;
        root=merge(x,y);
        return ans;
    }
};
FHQ_Treap treap;

int n;

int main(){
//    freopen("my.out","w",stdout);
    read(n);
    for(int i=1;i<=n;i++){
        int op,x;read(op);read(x);
        if(op==1) treap.insert(x);
        else if(op==2) treap.delete_(x);
        else if(op==3) printf("%d\n",treap.getrank(x));
        else if(op==4) printf("%d\n",treap.getval(x));
        else if(op==5) printf("%d\n",treap.getpre(x));
        else if(op==6) printf("%d\n",treap.getnext(x));
    }
    return 0;
}

引用

当你想要结束的时候,想想你为什么开始!

posted @ 2021-06-19 15:02  hyl天梦  阅读(409)  评论(1编辑  收藏  举报