FHQ Treap基本原理

FHQ Treap

视频讲解
优质讲解

一、普通Treap

Treap= Tree + Heap
在平衡树的每个节点存放两个信息:


  • 值满足二叉搜索树(BST)的性质

  • 随机修复值fix
    修复值满足堆的性质

  • 二叉搜索树的性质
    当前结点左子树的值都比当前结点值小,反之则一定在右子树上。

  • 二叉堆的性质(大根堆)
    父结点的优先级总是大于或等于任何一个子节点的优先级。
    这里提到的优先级,就是上面提到的索引。

总结
在保留二叉搜索树的中序遍历不变的前提下,采用一定的策略(二叉堆+随机数),使树尽量的均衡,就是平衡树的本质,中庸之道也~

二、Treap为什么可以平衡?

我们发现,BST会遇到不平衡的原因是因为有序的数据会使查找的路径退化成链
而随机的数据使BST退化的概率是非常小的
Treap中,修正值的引入恰恰是使树的结构不仅仅取决于节点的值,还取决于修正值的值
然而修正值的值是随机生成的,出现有序的随机序列是小概率事件,
所以Treap的结构是趋向于随机平衡的。

三、FHQ Treap

优点:码量小而好写,核心操作的代码都是复读机
好理解、支持的操作多

缺点:常数略大(很大~)

平衡树双子星,很牛B的样子。

四、奇怪的操作

普通Treap用来维护树平衡的 奇怪的操作是什么呢?
树旋转

FHQ Treap的奇怪操作并且是核心操作只有两个:
split , merge

只需掌握好这两个基本操作,那么你基本上就已经掌握了FHQ Treap了。

五、FHQ存储的信息

结点信息(5个):
左右子树编号 这个所有平衡树都一样
val
修复值(随机) fix
子树大小 size

六、分裂(split)

分裂有两种:按值分裂和按大小分裂

按值分裂:把树拆成两棵树,拆出来的一棵树的值全部小于等于给定的值,另外一部分全都大于给定的值。

按大小分裂:把树拆成两棵树,拆出来的一棵树的值全部小于给定的大小,另外一部分的值全部大于等于给定的大小。

一般当平衡树来用的时候,用的是按值分裂。 在维护区间信息的时候,采用按大小分裂。很经典的例子就是文艺平衡树,这个稍后再说。

七、合并(merge)

八、代码模板

P3369 【模板】普通平衡树
AcWing 253 普通平衡树

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int INF = 0x3f3f3f3f;


mt19937 rnd(233); //高性能随机数生成器 随机范围大概在(maxint,+maxint),233为种子,19937指该随机数循环节为2^19937

//重定义ls,rs
#define ls tr[p].l
#define rs tr[p].r

struct Node {
    int l, r; //左右儿子的节点号
    int rnd;  //在堆中编号,随机值
    int size; //以当前节点为根的子树中节点的个数
    int val;  //值
    //这里没有记录当前节点值的个数,是不是有点奇怪?
} tr[N];

int root, idx;

//向父节点更新统计信息
void pushup(int p) {
    tr[p].size = tr[ls].size + tr[rs].size + 1;
}

//创建一个新节点
int newnode(int val) {
    tr[++idx].rnd = rand();
    tr[idx].size = 1;
    tr[idx].val = val;
    return idx;
}

//将以p为根的平衡树进行分裂,小于等于x的都放到以pl为根的子树中,大于x都放到以pr为根的子树中
//因为生成的两个子树最终需要返回两个根,所以这里用了引用的方式
void split(int p, int val, int &x, int &y) {
    if (!p) { //当前节点为空,还分裂个屁~
        x = y = 0;
        return;
    }
    if (tr[p].val <= val)             //当前点归左边,如果当前点<=x,那么它的左子必然也<=x
        x = p, split(rs, val, rs, y); //把当前点p接在pl上,继续分它的右子,它的右子可能还有>x的
    else
        y = p, split(ls, val, x, ls); //把当前点p接在pr上,继续分它的左子,它的左子可能还有<=x的

    //推送统计信息
    pushup(p);
}
//将以pl,pr为根的两个子树合并成一棵树
//要求:两个子树的值域不能重叠,没有交叉
int merge(int x, int y) {
    if (!x || !y) return x + y;      //如果pl或者pr有一个是空了,那么返回另一个即可,此处比较取巧,采用了+
    int p;                           //根
    if (tr[x].rnd < tr[y].rnd) {     //两个都不空,谁来当根呢?需要使用小根堆性质,rk小的当根
        p = x;                       // x当根
        tr[x].r = merge(tr[x].r, y); //将y接到x的右子上
    } else {
        p = y;                       // y当根
        tr[y].l = merge(x, tr[y].l); //将x接到y的左子上
    }
    //更新统计信息
    pushup(p);
    return p;
}

//插入操作,插入一个数字val
void insert(int val) {
    int x, y, z;
    split(root, val, x, z);       // 1、小于等于val的划到x子树中,大于val的划到y子树中
    y = newnode(val);             // 2、创建一个新节点y
    root = merge(x, merge(y, z)); // 3、合并三者,因为这三者是需要满足BST有序的,所以顺序不能乱,更新根节点
}

//移除值val
void remove(int val) {
    int x, y, z;
    split(root, val, x, z);       // 1、小于等于v的放x里,大于v的放z里
    split(x, val - 1, x, y);      // 2、继续分裂x,小于等于v-1的放x里,大于v-1的放y里,即y里全部都是等于val的!
    y = merge(tr[y].l, tr[y].r);  // 3、y的左子树和右子树合并,相当于把根节点不要了
    root = merge(x, merge(y, z)); // 4、合并三者,更新根节点
}

//查询某个值的排名
int get_rank_by_key(int val) {
    int x, y;
    split(root, val - 1, x, y); // 1、按val-1分裂,此时x中就是所有<=val-1的数
    int res = tr[x].size + 1;   // 2、统计x为根的树中数字个数+1就是最终排名
    root = merge(x, y);         // 3、还原,这也太暴力了吧~
    return res;                 // 4、返回排名
}

//查询排名的值
int get_key_by_rank(int k) {
    int p = root;
    while (p) {                   // 1、如果当前节点不为空
        if (tr[ls].size + 1 == k) // 2、说明当前节点就是要查找的第k个数
            return tr[p].val;
        if (tr[ls].size >= k) // 3、如果左子树的数字数量大于等于k,在左子树中查找
            p = tr[p].l;
        else
            k -= tr[ls].size + 1, p = rs; // 4、换算下在右子树中查找
    }
    return -1; // 5、没有找到返回-1
}

//寻找v的前驱
int get_prev(int val) {
    int x, y;
    split(root, val - 1, x, y); // 1、按v-1分裂,x里的最大的数就是val的前驱
    int p = x;                  // 2、目标肯定在左子中
    while (rs) p = rs;          // 3、左子的最右边就是答案
    int res = tr[p].val;        // 4、记录答案
    root = merge(x, y);         // 5、还原回去
    return res;
}

//寻找v的后继
int get_next(int val) {
    int x, y;
    split(root, val, x, y); // 1、按v分裂,y里的最小的就是val的后继
    int p = y;              // 2、答案肯定在右子中
    while (ls) p = ls;      // 3、右子的最左边就是答案
    int res = tr[p].val;    // 4、记录答案
    root = merge(x, y);     // 5、还原回去
    return res;
}

int main() {
    // fhq可以有重复的点

    //为了防止找前驱后继时要找的数比FHQ中的数都小/大(比如我们要找FHQ最小的数的前驱)
    //我们可以一开始就加入−∞,∞两个哨兵节点。
    insert(-INF), insert(INF);

    int q;
    scanf("%d", &q);
    while (q--) {
        int op, x;
        scanf("%d%d", &op, &x);
        if (op == 1)
            insert(x);
        else if (op == 2)
            remove(x);
        else if (op == 3)
            printf("%d\n", get_rank_by_key(x) - 1);
        else if (op == 4)
            printf("%d\n", get_key_by_rank(x + 1));
        else if (op == 5)
            printf("%d\n", get_prev(x));
        else if (op == 6)
            printf("%d\n", get_next(x));
    }
    return 0;
}
posted @   糖豆爸爸  阅读(78)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2018-05-10 今天需要完成的开发任务
2013-05-10 使用PowerDesigner生成数据库测试数据
2013-05-10 开发流程与各层软件选型
2013-05-10 MySQL分区和分布性能测试[转]
2013-05-10 Innodb共享表空间VS独立表空间
2013-05-10 MySQL MyISAM与Innodb优化方案比较
Live2D
点击右上角即可分享
微信分享提示