Treap 学习笔记


Treap

Treap,顾名思义就是 Tree + Heap。这么命名的原因就是它使用了二叉堆的性质来保持二叉树的平衡。
我们知道,一个二叉(小根)堆满足这样的性质:一个节点的两个儿子的值都小于节点本身的值。如果一个二叉查找树满足这样的性质,那么它就被称作 Treap。
Treap 中每个节点有 2 个值,其中一个满足二叉查找树的性质,一个满足大根堆的性质。把满足二叉查找树性质的值称作 w,把满足大根堆性质的值称作 prio。 对于 Treap 来说,当前节点的 w 值大于左儿子,小于右儿子。当前节点的 prio 值小于儿子节点的值。
每个节点的 w 我们无法改变,为了保证 Treap 的平衡性,我们需要让每个节点的 prio 都取一个随机值,这样我们就可以保证这棵树“基本平衡”。


随机数的生成

如果我们采用系统函数生成的随机数,会有出现重复的可能性。如果 prio 取到了重复的值,则必然会造成堆结构的混乱。
生成不重复的随机数的办法:

inline int random_f() {
    static int seed = 623; // seed 可以随便取?
    return seed = int(seed * 48271LL % 2147483647);
}

48271 就可以取遍 1 - 2147483647 中的所有数字。


插入

给节点随机分配一个优先级,先把要插入的点插入到一个叶子上,然后跟维护堆一样,我们维护一个小根堆,如果当前节点的优先级比根大就旋转,如果当前节点是根的左儿子就右旋,如果当前节点是根的右儿子就左旋。

由于旋转是 O(1) 的,最多进行 h 次(h 是树的高度),插入的复杂度是 O(h) 的,在期望情况下 h=O(logn) ,所以它的期望复杂度是 O(logn)


其实也就是说,
右旋就是,让当前节点降为自己的右儿子,让左儿子代替自己,并让自己左儿子的右儿子成为自己的左儿子。
左旋相同,就是让当前节点降为自己的左儿子,让右儿子代替自己,并让自己右儿子的左儿子成为自己的右儿子。


删除

因为 Treap 满足堆性质,所以只需要把要删除的节点旋转到叶节点上,然后直接删除就可以了。

具体的方法:

如果该节点的左子节点的优先级小于右子节点的优先级,右旋该节点,使该节点降为右子树的根节点,然后访问右子树的根节点,继续操作;
反之,左旋该节点,使该节点降为左子树的根节点,然后访问左子树的根节点,继续操作,直到变成可以直接删除的节点。
(即:让优先级小的结点旋到上面,满足堆的性质)

删除最多进行 O(h) 次旋转,期望复杂度是 O(logn)


Luogu P3369 【模板】普通平衡树(Treap/SBT)

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

using namespace std;
const int N = 2e5 + 5; 

inline int random_f() {
    static int seed = 623;
    return seed = int(seed * 48271LL % 2147483647);
}

struct Treap {
    int rt, cnt;
    int w[N], prio[N], size[N], lson[N], rson[N];

    inline void clear() {
        rt = 0; cnt = 0;
        memset(w, 0, sizeof(w));
        memset(prio, 0, sizeof(prio));
        memset(size, 0, sizeof(size));
        memset(lson, 0, sizeof(lson));
        memset(rson, 0, sizeof(rson));
    }

    inline void pushup(int &p) {size[p] = size[lson[p]] + size[rson[p]] + 1;}

    inline void right_rorate(int &p) {
        int tmp = lson[p];
        lson[p] = rson[tmp];
        rson[tmp] = p;

        size[tmp] = size[p];
        pushup(p);
        p = tmp;
    }

    inline void left_rorate(int &p) {
        int tmp = rson[p];
        rson[p] = lson[tmp];
        lson[tmp] = p;

        size[tmp] = size[p];
        pushup(p);
        p = tmp;
    }

    inline void insert(int &p, int x) {
        if(p == 0) {
            p = ++ cnt;
            size[p] = 1, w[p] = x, prio[p] = random_f();
            return ;
        }

        size[p] ++;
        insert((x >= w[p]) ? rson[p] : lson[p], x);
        if(prio[lson[p]] < prio[p] && lson[p] != 0) right_rorate(p);
        if(prio[rson[p]] < prio[p] && rson[p] != 0) left_rorate(p);
        pushup(p);
    }

    inline void delete_f(int &p, int x) {
        size[p] --;

        if(w[p] == x) {
            if(lson[p] == 0 && rson[p] == 0) {p = 0; return ;}
            if(lson[p] == 0 || rson[p] == 0) {p = lson[p] + rson[p]; return ;}

            if(prio[lson[p]] < prio[rson[p]]) {
                right_rorate(p);
                delete_f(rson[p], x);
                return ;
            } else {
                left_rorate(p);
                delete_f(lson[p], x);
                return ;
            }
        }

        delete_f((x >= w[p]) ? rson[p] : lson[p], x);
        pushup(p);
    }   

    inline int rank(int &p, int x) {
        if(p == 0) return 0;

        int tmp;
        if(x > w[p]) tmp = size[lson[p]] + 1 + rank(rson[p], x);
        else tmp = rank(lson[p], x);

        return tmp;
    }

    inline int select(int &p, int x) {
        if(x == size[lson[p]] + 1) return w[p]; 

        if(x > size[lson[p]] + 1) return select(rson[p], x - size[lson[p]] - 1);
        else return select(lson[p], x);
    }

    inline int querypre(int &p, int x) {
        if(p == 0) return 0; // 找不到超过 x 的,就返回 0,tmp 的 0 是在这里赋值的 

        if(w[p] >= x) return querypre(lson[p], x);
        // 此时 w[p] < x,再到 p 的右子树里找一个最大的不超过 x 的,即为答案
        int tmp = querypre(rson[p], x);
        return (tmp == 0) ? w[p] : tmp;
    }

    inline int querysuf(int &p, int x) {
        if(p == 0) return 0;

        if(w[p] <= x) return querysuf(rson[p], x);
        int tmp = querysuf(lson[p], x);
        return (tmp == 0) ? w[p] : tmp;
    }
}treap;

int main() {
    int n;
    scanf("%d", &n);
    treap.clear();

    for(int i = 1; i <= n; i ++) {
        int opt, x;
        scanf("%d%d", &opt, &x);

        if(opt == 1) treap.insert(treap.rt, x);
        if(opt == 2) treap.delete_f(treap.rt, x);
        if(opt == 3) printf("%d\n", treap.rank(treap.rt, x) + 1);
        if(opt == 4) printf("%d\n", treap.select(treap.rt, x));
        if(opt == 5) printf("%d\n", treap.querypre(treap.rt, x));
        if(opt == 6) printf("%d\n", treap.querysuf(treap.rt, x));
    }
    return 0;
}
可持久化splay是一种数据结构,它是对splay树进行修改和查询的一种扩展。在传统的splay树中,对树的修改操作会破坏原有的树结构,而可持久化splay树则允许我们对树进行修改、查询,并且可以保存修改后的每个版本的树结构。 在可持久化splay树中,我们不会直接对原树进行修改,而是通过复制每个节点来创建新的版本。这样,每个版本都可以独立地修改和查询,保留了原有版本的结构和状态。每个节点保存了其左子树和右子树的引用,使得可以在不破坏原有版本的情况下进行修改和查询。 为了实现可持久化splay树,我们可以使用一些技巧,比如引用中提到的哨兵节点和假的父节点和孩子节点。这些技巧可以帮助我们处理根节点的旋转和其他操作。 此外,可持久化splay树还可以与其他数据结构相结合,比如引用中提到的可持久化线段树。这种结合可以帮助我们解决更复杂的问题,比如区间修改和区间查询等。 对于可持久化splay树的学习过程,可以按照以下步骤进行: 1. 理解splay树的基本原理和操作,包括旋转、插入、删除和查找等。 2. 学习如何构建可持久化splay树,包括复制节点、更新版本和保存历史版本等。 3. 掌握可持久化splay树的常见应用场景,比如区间修改和区间查询等。 4. 深入了解与可持久化splay树相关的其他数据结构和算法,比如可持久化线段树等。 在解决问题时,可以使用二分法来确定答案,一般称为二分答案。通过对答案进行二分,然后对每个答案进行检查,以确定最终的结果。这种方法可以应用于很多问题,比如引用中提到的在线询问问题。 综上所述,可持久化splay是一种对splay树进行修改和查询的扩展,可以通过复制节点来创建新的版本,并且可以与其他数据结构相结合解决更复杂的问题。学习过程中可以按照一定的步骤进行,并且可以使用二分法来解决一些特定的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[学习笔记]FHQ-Treap及其可持久化](https://blog.youkuaiyun.com/weixin_34283445/article/details/93207491)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [可持久化数据结构学习笔记](https://blog.youkuaiyun.com/weixin_30376083/article/details/99902410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值