平衡树之FHQ Treap(无旋)

本文介绍了FHQ Treap的基本概念及实现原理,包括其与普通Treap的区别、关键操作如分裂与合并的具体实现,以及如何利用这些操作完成插入、删除等操作。

2022年01月15日,第二天

平衡树——FHQ TreapFHQ\ TreapFHQ Treap(无旋)

学习 fhq Treapfhq\ Treapfhq Treap 之前,要先了解普通的 TreapTreapTreap 是怎么回事。

TreapTreapTreap ,就是 TreeTreeTreeHeapHeapHeap 。 它让平衡树上的每一个结点存放两个信息:值和一个随机的索引。其中值满足二叉搜索树的性质,索引满足堆的性质,结合二叉搜索树和二叉堆的性质来使树平衡。这也是 TreapTreapTreap 的性质。

TreapTreapTreap 为什么可以平衡?我们都知道,如果对一颗二叉搜索树进行插入的值按次序是有序的,那么二叉搜索树就会退化成一个链表。那么我们就可以别让数值按次序插入,一个很好的方法就是把插入的数值随机化,原因是大量数据时,随机化后变成有序的概率几乎为零。

TreapTreapTreap 用二叉堆来维护随机索引,其实就是相当于把插入次序随机化。插入一数值后你必然要让索引满足二叉堆的特性,但又因为索引是随机的,那就会导致插入的数不知道搞到了哪里去了,相当于插入次序随机了。

二叉搜索树的性质:当前结点左子树的值都比当前结点的值小,右子树的值都不比当前结点小,也就是大于等于。

二叉堆的性质:父结点的 优先级 总是大于或等于任何一个子节点的 优先级

普通的 TreapTreapTreap 用来维护树平衡的核心操作是树旋转,而 fhq Treapfhq\ Treapfhq Treap 的核心操作有两个,分别是 分裂 (split)(split)(split)合并 (merge) ,常数稍大。

结点,fhq Treapfhq\ Treapfhq Treap 的结点需要维护一下五个信息:

  • 左右子树编号
  • 元素的值
  • 索引
  • 子树大小
struct node {
    int l, r;
    int val, key;
    int size;
}fhq[N];
int cnt, root;
#include <random>
mt19937 rnd(233);
inline int newnode(int val) {
    fhq[++ cnt].val = val;
    fhq[cnt].key = rnd();
    fhq[cnt].size = 1;
    return cnt;
}
核心操作——分裂 (split)(split)(split)

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

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

按大小分裂:把树拆成两颗树,其中一颗树的大小等于给定的大小,剩余部分在另一颗数里。

一般来说,我们在使用 fhq Treapfhq\ Treapfhq Treap 当一颗正常的平衡树用的时候,使用按值分裂,而在维护区间信息的时候,使用按大小分裂。很经典的例子就是 文艺平衡树

inline void pushup(int now) {
    fhq[now].size = fhq[fhq[now].l].size + fhq[fhq[now].r].size + 1;
}
void split (int now, int val, int& x, int& y) {
    if (! now) x = y = 0;
    else {
        if (fhq[now].val <= val) {
            x = now;
            split(fhq[now].r, val, fhq[now].r, y);
        } else {
            y = now;
            split(fhq[now].l, val, x, fhq[now].l);
        }
        pushup(now);
    }
}
核心操作——合并 (merge)(merge)(merge)

本质上是分裂的逆向操作。

int merge(int x, int y) {
    if (! x || ! y) return x | y;
    if (fhq[x].key > fhq[y].key) {
        fhq[x].r = merge(fhq[x].r, y);
        pushup(x);
        return x;
    } else {
        fhq[y].l = merge(x, fhq[y].l);
        pushup(y);
        return y;
    }
}
第一个操作:插入

设插入的值为 valvalval ,那么我们的步骤就是:按值 valvalval 把树分裂成 xxxyyy ,合并 xxx ,新结点,yyy

可以这么做的原因:我们要插入 valvalval 这个值,那么我们按值 valvalval 分裂,于是得到了两颗树 xxxyyy ,按照按值分裂的定义,我们分裂出来的 xxx 树上的所有值一定都小于等于 valvalvalyyy 树上的所有值一定都大于 valvalval ,那么我们就可直接合并 xxx ,用值 valvalval 新建的新结点,yyy 就可以了。

int x, y, z;
inline void insert (int val) {
    split (root, val, x, y);
    root = merge (merge (x, newnode(val)), y);
}
第二个操作:删除

设要删除的值为 valvalval ,那么:首先,按值 valvalval 把树分裂成 xxxzzz ,再按值 val−1val - 1val1 把树分裂成 xxxyyy ,那么此时 yyy 树上的所有值都是等于 valvalval 的,我们去掉它的根结点:让 yyy 等于合并 yyy 的左子树和 yyy 的右子树。

inline void del (int val) {
    split (root, val, x, z);
    split (x, val - 1, x, y);
    y = merge (fhq[y].l, fhq[y].r);
    root = merge (merge (x, y), z);
}
第三个操作:查询值的排名

设要查询的值为 valvalval ,那么:按值 val−1val-1val1 分裂成 xxxyyy ,则 xxx 的大小 +1+1+1 就是 valvalval 的排名,最后将 xxxyyy 合并起来。

inline int getrank (int val) {
    split (root, val - 1, x, y);
    int ans = fhq[x].size + 1;
    root = merge (x, y);
    return ans;
}
第四个操作:查询排名的值

和替罪羊树的做法一样。

inline int getnum (int rank) {
    int now = root;
    while (now) {
        if (fhq[fhq[now].l].size + 1 == rank) 
            break;
        else if (fhq[fhq[now].l].size >= rank) 
            now = fhq[now].l;
        else {
            rank -= fhq[fhq[now].l].size + 1;
            now = fhq[now].r;
        }
    }
    return fhq[now].val;
}
第五个操作:前驱和后继

设操作数为 valvalval ,前驱:按值 val−1val-1val1 分裂成 xxxyyy ,则 xxx 里面最右的数就是 valvalval 的前驱。后继:按值 valvalval 分裂成 xxxyyy ,则 yyy 里面最左的数就是 valvalval 的后继。

inline int pre (int val) {
    split (root, val - 1, x, y);
    int now = x;
    while (fhq[now].r) 
        now = fhq[now].r;
    int ans = fhq[now].val;
    root = merge (x, y);
    return ans;
}
inline int nxt (int val) {
    split (root, val, x, y);
    int now = y;
    while (fhq[now].l)
        now = fhq[now].l;
    int ans = fhq[now].val;
    root = merge (x, y);
    return ans;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值