简洁好懂的Fhq Treap讲解

首先
如果你不知道 T r e a p Treap Treap
那么
快滚去学Treap
下面这些可以算是预备知识
因为了解 T r e a p Treap Treap的话 F h q Fhq Fhq理解起来也就会快
当然从零开始也是可以的
可以跳过这一部分

在这简单说一下好了
T r e a p Treap Treap B S T BST BST的基础上,添加了一个修正值。
如果还不太懂 B S T BST BST的话
来这看看吧———传送门
在满足 B S T BST BST性质的基础上, T r e a p Treap Treap节点的修正值还满足最小堆性质
最小堆性质可以被描述为每个子树根节点都小于等于其子节点
然后它的性质:

  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,而且它的根节点的修正值小于等于左子树根节点的修正值
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值,而且它的根节点的修正值小于等于右子树根节点的修正值
  3. 它的左、右子树也分别为 T r e a p Treap Treap

那么
说了这些
F h q Fhq Fhq T r e a p Treap Treap和普通的 T r e a p Treap Treap的区别和联系在哪呢?
最重要的一点
F h q Fhq Fhq中没有旋转操作
也叫无旋 T r e a p Treap Treap
T r e a p Treap Treap需要大量的旋转操作来维持自身的平衡
只有左旋和右旋
s p l a y splay splay要简单一些
F h q Fhq Fhq的第一个优势就出来了:
代码量少
它的主要操作就是两个
s p l i t split split m e r g e merge merge
分别是分裂合并
不信的自己百度
下面一个一个说
(当然还有其他的常见操作)


忘了还没说初始化
如果你不开结构体的话
Luogu 3369 普通平衡树这道题
只需要一个记左右儿子的数组 c h [ ] [ 0 / 1 ] ch[][0/1] ch[][0/1]
记子树大小的数组 s i z [ ] siz[] siz[](包括自身)
记权值的数组 v a l [ ] val[] val[]
还有一个记修正值的数组 c v [ ] cv[] cv[] C o r r e c t i o n Correction Correction V a l u e Value Value
下面的讲解是数组版的
指针党相信你们有能力自己写出来
下面开始操作函数的讲解

split

核心思路:
将一棵树拆分成一棵所有节点的值均小于 k k k的树和一棵所有节点的值均大于 k k k的树
空树将被拆分成两棵空树(边界条件)
在递归过程中,若访问到一个节点的权值大于 k k k节点的权值
则说明该节点右子树中所有节点均比 k k k
则将其左子树拆分成一棵所有节点的值均小于 k k k的树和一棵所有元素均大于 k k k的树
并将其左子树拆分出的大于 k k k的树作为该节点的左儿子
则我们得到的两棵树为,
以该节点为根的所有节点的值均大于 k k k的树,
和该节点左子树中部分节点构成的所有节点的值均小于 k k k的树
反之,
若访问到一个节点的值小于 k k k的节点
则说明该节点左子树中所有节点的值均小于 k k k
则将其右子树拆分成一棵所有元素的值均小于 k k k的树和一棵所有元素的值均大于 k k k的树
并将其右子树拆分出的那棵所有元素的值均小于 k k k的树作为该节点的右子树
则我们得到的两棵树为,
以该节点为根的所有元素均小于 k k k的树,
和该节点右子树中部分节点构成的所有元素均大于 k k k的树

void split(int now, int k, int &x, int &y) {
    if (!now) { //当前子树是空的,直接返回就好了(边界条件,除了一开始root为0时)
        x = y = 0;
        return;
    }
    if (val[now] <= k) { //当前权值与基准数作比较,如果比基准数小
        x = now; //当前节点和它的左子树分到左边的树中去
        split(ch[now][1], k, ch[now][1], y); //以now的右儿子为根递归右子树
    }
    else {
        y = now; //当前节点和它的右子树分到右边的树中去
        split(ch[now][0], k, x, ch[now][0]); //以now的左儿子为根递归左子树
    }
    update(now); //最后要更新子树大小等信息
}

看参数

简要概括

s p l i t split split函数做的就是把以 n o w now now为根的子树按 k k k为基准数,比 k k k小的分到左子树 x x x,否则分到右子树 y y y
然后 n o w now now这棵树就被分成了 x x x y y y这两棵树
而且 x x x树里的所有点的权值小于 y y y树里的所有点的权值(因为这是二叉搜索树)
在一篇博客中还看到一种比较清楚的解释:
比较当前根节点权值 v a l [ n o w ] val[now] val[now] k k k
若大于,则根节点和右子树被划分为 y y y树,当前根为 n o w now now y y y树的左子树不确定,进入左子树求解子问题
若不大于,则根节点和左子树被划分为 x x x树,当前根为 n o w now now x x x树的右子树不确定,进入右子树求解子问题

上面代码因为要加注释
太长了,缩一下,也就是我日常的写法

void split(int now, int k, int &x, int &y) {
    if (!now) {x = y = 0; return;}
    if (val[now] <= k) x = now, split(ch[now][1], k, ch[now][1], y);
    else y = now, split(ch[now][0], k, x, ch[now][0]);
    update(now);
} //这样还是挺漂亮的

注意上面这是按权值分裂
排名(size)分裂简单提一下
思想是一样的
把前面的 k k k个放到左子树里
后面的都放在右子树里

void split(int now, int k, int &x, int &y) {
    if (!now) {x = y = 0; return;}
    if (k <= siz[ch[now][0]]) y = now, split(ch[now][0], k, x, ch[now][0]);
    else x = now, split(ch[now][1], k - siz[ch[now][0]] - 1, ch[now][1], y);
    update(now);
}

merge

F h q Fhq Fhq的另一个重要操作,合并
T r e a p Treap Treap中,父节点的修正值小于两个子节点的修正值

int merge(int x, int y) { //注意这个操作的返回值是根节点
    if (!x or !y) return x + y; //如果有一棵树是空的,返回另一棵树就可以
    if (cv[x] < cv[y]) { //比较修正值
        tree[x][1] = merge(tree[x][1], y); //把y合并到x的右子树
        update(x);
        return x;
    }
    else {
        tree[y][0] = merge(x, tree[y][0]); //把x合并到y的左子树
        update(y);
        return y;
    }
}

m e r g e merge merge函数是把以 x x x为根的子树和以 y y y为根的子树合并,返回新的根节点
注意以 x x x值为根的子树的最大节点小于以 y y y为根的子树的最小节点(因为二叉查找树左右孩子的定义)
也就是说这两棵子树本来就是一棵完全小于另外一棵
所以我们只需要在合并时保证堆性质即可
这个相对来说好理解

然后就是对题目而言的其他操作
下面先说完其他的附属函数

update

记得每次重构完子树都 u p d a t e update update一下

void update(int x) {
    siz[x] = 1 + siz[tree[x][0]] + siz[tree[x][1]];
}
randoom

这里的随机数你直接用 r a n d ( ) rand() rand()也可以
但是在效率上会有差别
空间和时间上手写的都会更优
特别是空间上

int randoom() {
    return rand() << 15 | rand();
}
newnode

就是新建节点

int newnode(int x) {
    siz[++cnt] = 1; //子树大小默认为1
    val[cnt] = x; //记下权值
    cv[cnt] = randoom(); //随机一个修正值
    return cnt; //返回根
}

在这里介绍Luogu 3369 普通平衡树中的几种操作

插入 a a a

这些就不用写函数了
直接在输入后面用就行
r o o t root root刚开始默认为 0 0 0

    split(root, a, x, y); //先把树分裂开
    root = merge(merge(x ,newnode(a)), y); //合并后返回新根的编号

删除 a a a

    split(root, a, x, z); //分裂开原树
    split(x, a - 1, x, y); //在按a-1分裂一次,这就只孤立出了要删的那个节点
    y = merge(tree[y][0], tree[y][1]); //把其余的合并起来
    root = merge(merge(x, y), z); //返回新根

查询 a a a的排名

    split(root, a - 1, x, y); //很显然
    printf("%d\n", siz[x] + 1); //下面的子树大小就是a的排名
    root = merge(x, y);

查询排名为 a a a的数

    printf("%d\n", val[kth(root, a)]); //找到这个点输出出来就好

a a a的前驱

    split(root, a - 1, x, y); //自己感性理解
    printf("%d\n", val[kth(x, siz[x])]);
    root = merge(x, y);

a a a的后继

    split(root, a, x, y); //同上qwq
    printf("%d\n", val[kth(y, 1)]);
    root = merge(x, y);

Luogu 3369 普通平衡树:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <complex>
#include <algorithm>
#include <climits>
#include <queue>
#include <map>
#include <set>
#include <vector>
#include <iomanip>
#define A 1000010
#define B 2010

using namespace std;
typedef long long ll;
int tree[A][2], val[A], cv[A], siz[A], cnt;
int n, a, opt, root, x, y, z;
void update(int x) {
    siz[x] = 1 + siz[tree[x][0]] + siz[tree[x][1]];
}
int randoom() {
    return rand() << 15 | rand();
}
int newnode(int x) {
    siz[++cnt] = 1;
    val[cnt] = x;
    cv[cnt] = randoom();
    return cnt;
}
void split(int now, int k, int &x, int &y) {
    if (!now) {x = y = 0; return;}
    if (val[now] <= k) x = now, split(tree[now][1], k, tree[now][1], y);
    else y = now, split(tree[now][0], k, x, tree[now][0]);
    update(now);
}
int merge(int x, int y) {
    if (!x or !y) return x + y;
    if (cv[x] < cv[y]) {
        tree[x][1] = merge(tree[x][1], y);
        update(x);
        return x;
    }
    else {
        tree[y][0] = merge(x, tree[y][0]);
        update(y);
        return y;
    }
}
int kth(int now, int k) {
    if (k <= siz[tree[now][0]]) kth(tree[now][0], k);
    else if (k == siz[tree[now][0]] + 1) return now;
    else kth(tree[now][1], k - siz[tree[now][0]] - 1);
}

int main() {
    srand(time(0));
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d%d", &opt, &a);
        switch(opt) {
            case 1 : split(root, a, x, y); root = merge(merge(x ,newnode(a)), y); break;
            case 2 : split(root, a, x, z); split(x, a - 1, x, y); y = merge(tree[y][0], tree[y][1]); root = merge(merge(x, y), z); break;
            case 3 : split(root, a - 1, x, y); printf("%d\n", siz[x] + 1); root = merge(x, y); break;
            case 4 : printf("%d\n", val[kth(root, a)]); break;
            case 5 : split(root, a - 1, x, y); printf("%d\n", val[kth(x, siz[x])]); root = merge(x, y); break;
            case 6 : split(root, a, x, y); printf("%d\n", val[kth(y, 1)]); root = merge(x, y); break;
            default : break;
        }
    }
    return 0;
}
  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
可持久化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.csdn.net/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.csdn.net/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 ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

良月澪二

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值