『学习笔记』fhq-treap(无旋treap)

模板题:洛谷 P6136 【模板】普通平衡树(数据加强版)

(emm刚写了这个,就放这个吧)

\(fhq-treap\) 也叫做 无旋treap,由防火墙范浩强大佬发明。

个人认为是平衡树中码量最少,也最容易理解的一种写法。

主要思想

顾名思义,无旋意味着它没有旋转操作(终于没有恶心人的旋转了)。

事实上,无旋treap只有两种操作。

一种是 \(split\)(分裂),另一种是 \(merge\)(合并)。

平衡树中的各种操作都可以用这两个函数来搞定。下面我们来一一分析。

常见操作

  • 插入一个整数 \(x\)

  • 删除一个整数 \(x\)(若有多个相同的数,只删除一个)。

  • 查询整数 \(x\) 的排名(排名定义为比当前数小的数的个数 +1)。

  • 查询排名为 \(x\) 的数(如果不存在,则认为是排名小于 \(x\) 的最大数。保证 \(x\) 不会超过当前数据结构中数的总数)。

  • \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)。

  • \(x\) 的后继(后继定义为大于 \(x\),且最小的数)。

当然区间反转也是可以用无旋\(treap\) 实现的,这里先不讲。

函数解释

split(分裂)

\(split\) 函数要实现把一棵平衡树拆成两棵,左边的树上节点的值都小于等于 \(k\), 右边的树上节点的值都大于等于 \(k\)

  • \(x:\) 当前子树根节点

  • \(k:\)\(k\) 为分界线分割树

  • \(a, b:\) 分裂之后两棵子树的根,左边的子树根是 \(a\),右边的根是 \(b\),这里要传地址,方便写。

注意: 当根为 0 时,一定一定一定要清空 \(a\),&b&,不然会死循环(我就在这里卡了好久QWQ)

inline void split(int x, int k, int &a, int &b){
    if(!x){
        a = b = 0;    //清空清空清空(重要的事情说三遍)
        return;
    }
    if(t[x].val <= k){
        a = x;        //左边子树已经分裂出去了,直接a=x
        split(rs(x), k, rs(x), b);    //在右子树上查找,把拆下来右子树上节点值<=k的点放回到x的右子树上
    }else{
        b = x;        //同理
        split(ls(x), k, a, ls(x));
    }
    pushup(x);
}

merge(合并)

\(merge\) 函数要实现把两个分开的树再合并到一起去,并返回合并后的根节点。

  • \(x, y:\) 两棵树的根

这个操作类似于线段树合并。

注意合并时只能是两个相邻的子树合并,不能跳着合并。

例如:树 \(rt\) ---> 子树\(a\) 和 子树\(b\)

子树\(b\) ---> 子树\(c\) 和 子树\(d\) (---> 表示拆成两棵树)

那么我们合并时,只能 \(merge(a, merge(c,d))\)\(merge(merge(a, c), d)\)

而不能 \(merge(merge(a, d), c)\)

inline int  merge(int x, int y){
    if(!x || !y) return x + y;
    if(t[x].wei <= t[y].wei){        //wei是随机值,wei(x) <= wei(y) 时,让 y 作为 x 的子树
        rs(x) = merge(rs(x), y);
        pushup(x);
        return x;
    }else{                            //反之,x 作为 y 的子树
        ls(y) = merge(x, ls(y));
        pushup(y);
        return y;
    }
}

insert(插入)

先新建一个点设为 \(y\),把原树拆成 \(x\)\(z\)

\(x\) 中节点值 \(<= k\)\(z\) 中节点值 \(>k\)

然后一次合并 \(x\)\(y\)\(z\)即可。

inline void insert(int k){
    t[++tot].val = k, t[tot].wei = rand();
    t[tot].siz = 1;
    split(root, k, a, b);
    root = merge(merge(a, tot), b);
}

remove(删除)

这个就很巧妙了,我们把值 \(<=k\) 的树先拆出来设为 \(a\),然后把值 \(<k\) 的点再拆出来,剩余的部分设为为 \(c\)

那么现在 \(c\) 中的点就是 \(=k\) 的。

我们直接合并 \(c\) 的左右两棵子树,这样就相当于把根节点删了。

最后合并回去。

inline void remove(int k){
    split(root, k, a, b);        //拆,<=k的存到a里,>k的存到b里。
    split(a, k - 1, a, c);        //再拆,<k的存到a里,=k的存到里。
    c = merge(ls(c), rs(c));    //合并c左右子树
    root = merge(merge(a, c), b);    //再把a,b合并上去。
}

check_rk

查询 \(k\) 的排名。

我们把 \(<k\) 的的点从树上分裂出来,然后分出来的树的大小 +1 就是排名。

别忘了合并回去。

inline int check_rk(int k){
    int rank;
    split(root, k - 1, a, b);
    rank = t[a].siz + 1;
    root = merge(a, b);
    return rank;
}

check_val

查询排名为 \(k\) 的数。

我这里用的递归写法。

  • \(x\) 当前子树的根。

  • \(k\) 在当前子树中排名多少。

我这里用的递归写法。(代码应该挺好理解的吧)

inline int check_val(int x, int k){
    if(k == t[ls(x)].siz + 1) return t[x].val;
    if(k <= t[ls(x)].siz) return check_val(ls(x), k);    //在左子树中,直接跑左子树里查
    else return check_val(rs(x), k - t[ls(x)].siz - 1);    //在右子树中,减去左子树大小,再减1(根),就是在右子树中的排名。
}

check_pre

查询前驱。

这个也好说,我们先查排名,设排名为 \(rk\),我们再查排名为 \(rk - 1\) 的数是多少,就是前驱了。

inline int check_pre(int x){
    return check_val(root, check_rk(x) - 1);
}

check_next

查询 \(x\) 的后继,基本同理,但略有不同。

因为 \(x\) 可能有多个,我们查 \(x\) 的排名后 +1,不一定是 \(x + 1\),有可能还是 \(x\)

那我们怎么办呢?

其实也不难,我们直接查 \(x + 1\) 的排名,没有?没关系,就算没有 \(x + 1\),查出来的也是大于 \(x\) 的最小的数的排名。

然后我们再查一下这个排名的值即可。

inline int check_next(int x){
    return check_val(root, check_rk(x + 1));
}

然后……就没有然后了吧。

主函数我就不用多说了吧。

完整代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#define ls(x) t[x].ch[0]
#define rs(x) t[x].ch[1]

using namespace std;

const int N = 2e6;        //原本的数+操作数。最多有1.1e6个数
struct Tree{
    int wei, val, siz, ch[2];
}t[N];
int n, m, tot, root;
int last, ans, a, b, c;

inline int read(){
    int x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
    return x * f;
}

inline void pushup(int x){
    t[x].siz = t[ls(x)].siz + t[rs(x)].siz + 1;
}

inline void split(int x, int k, int &a, int &b){
    if(!x){
        a = b = 0;
        return;
    }
    if(t[x].val <= k){
        a = x;
        split(rs(x), k, rs(x), b);
    }else{
        b = x;
        split(ls(x), k, a, ls(x));
    }
    pushup(x);
}

inline int  merge(int x, int y){
    if(!x || !y) return x + y;
    if(t[x].wei <= t[y].wei){
        rs(x) = merge(rs(x), y);
        pushup(x);
        return x;
    }else{
        ls(y) = merge(x, ls(y));
        pushup(y);
        return y;
    }
}

inline void insert(int k){
    t[++tot].val = k, t[tot].wei = rand();
    t[tot].siz = 1;
    split(root, k, a, b);
    root = merge(merge(a, tot), b);
}

inline void remove(int k){
    split(root, k, a, b);
    split(a, k - 1, a, c);
    c = merge(ls(c), rs(c));
    root = merge(merge(a, c), b);
}

inline int check_rk(int k){
    int rank;
    split(root, k - 1, a, b);
    rank = t[a].siz + 1;
    root = merge(a, b);
    return rank;
}

inline int check_val(int x, int k){
    if(k == t[ls(x)].siz + 1) return t[x].val;
    if(k <= t[ls(x)].siz) return check_val(ls(x), k);
    else return check_val(rs(x), k - t[ls(x)].siz - 1);
}

inline int check_pre(int x){
    return check_val(root, check_rk(x) - 1);
}

inline int check_next(int x){
    return check_val(root, check_rk(x + 1));
}

int main(){
    n = read(), m = read();
    for(int i = 1; i <= n; i++){
        int x;
        x = read();
        insert(x);
    }
    while(m--){
        int op, x;
        op = read(), x = read() ^ last;
        if(op == 1) insert(x);
        else if(op == 2) remove(x);
        else if(op <= 2) continue;
        else if(op == 3) last = check_rk(x);
        else if(op == 4) last = check_val(root, x);
        else if(op == 5) last = check_pre(x);
        else if(op == 6) last = check_next(x);
        ans ^= last;
    }
    printf("%d\n", ans);
    return 0;
}

\[\_EOF\_ \]

posted @ 2021-08-12 14:09  xixike  阅读(504)  评论(0编辑  收藏  举报