[模板] fhqTreap (非旋Treap)+ 可持久化平衡树

引言

  这两天跟着网上dalao学了fhqTreap,感觉十分强大。它和Splay一样, 可以维护队列或排序二叉树, 但只需要两个操作: merge m e r g e split s p l i t 就可以实现Splay所有功能, 而且不需要任何旋转! (博主蒟蒻每次写Splay都要调2h QAQ,不是RE就是TLE…)非旋Treap对于博主这样的手残党十分友好, 代码简短好记, 只是常数稍大。

下面博主来分别介绍非旋Treap的各种常见操作。(相信大家已经学过Treap了, 就不在这里废话)

我们需要的数组(可以开成结构体):

    int tree[MX << 1][2]//左右儿子
    , val[MX]//节点权值
    , siz[MX]//子树大小
    , pri[MX];//和Treap一样有优先级
题目传送门:排序二叉树 队列维护 可持久化平衡树
1. split

  顾名思义, split是将一棵Treap分成两棵, 而分的方式有两种: 按节点的权值划分(用于维护二叉平衡树性质) 和按子树大小划分(即固定一棵分裂出的Treap的大小, 用于维护序列)。
  首先我们需要两个节点 x x y, 作为分裂下的Treap的根节点,方便我们进行其他操作。为了统一,我们令大于划分值的节点都在以 y y 为根的树上,小于划分值的节点都在以x为根的子树上。然后我们从根节点开始向下, 若当前节点考虑的值小于划分考虑的值, 我们就可以将原Treap的右子树完全剖下, 作为 y y 树的右子树, 然后使原指向y节点的指针指向 y y 节点的左子树。反之就将原Treap的左子树全部剖下, 作为x树的左子树, 然后使原指向 x x 节点的指针指向x的右子树。

那么我们的y树剖出来会是这样的(相同颜色代表是原来Treap同一条链上的)

下面放出代码:

//维护排序二叉树版本
void split(int now, int tar, int &x, int &y)
    {//tree[x][0]代表x的左儿子, tree[x][1]代表x的右儿子
        if(!now) {x = y = 0; return;}//已经剖到最底层了
        if(val[now] <= tar) 
        {x = now, split(tree[now][1], tar, tree[now][1], y);}
        else {y = now, split(tree[now][0], tar, x, tree[now][0]);}
        pushup(now);
    }
//维护序列版本
void split(int now, int tar, int &x, int &y)
    {
        if(!now) {x = y = 0; return;}
        pushdown(now);//有标记需下传
        if (siz[tree[now][0]] >= tar) {y = now, split(tree[now][0], tar, x, tree[now][0]);}
        else {x = now, split(tree[now][1], tar - 1 - siz[tree[now][0]], tree[now][1], y);}
        pushup(now);
    }

非常简单有木有…

2.merge

  merge实际上是split的逆操作, 因为我们已经保证split的时候 y y 子树的权值都大于x子树的权值, 所以我们现在需要维护Treap堆的性质。在这里我们不妨设其满足小根堆的性质,从两棵剖下的树的树根开始, 每次比较两棵树节点的随机的优先值, 如果 prix<priy p r i x < p r i y 我们就应将 x x 的左子树接到当前节点的左边, 再将x的右子树继续与 y y 进行合并。反之我们就 将y的右子树接到当前节点的右边, 将 x x y的左子树继续合并。

代码如下:

int merge(int x, int y)
    {
        if(!x || !y) return x + y;//一棵树合并完成了, 返回另一棵树当前的指针值
        if(pri[x] < pri[y]) {tree[x][1] = merge(tree[x][1], y); pushup(x); return x;}//此情况下等于合成后的树左儿子就不变了, 右儿子递归合成。
        else {tree[y][0] = merge(x, tree[y][0]); pushup(y); return y;}
    }

和左偏树的合并过程差不多, 也非常简洁。

3.第k大

因为我们维护了每个节点的siz信息, 直接像splay一样递归下去找就行了

int kth(int k, int now)
    {
        if(k == siz[tree[now][0]] + 1) return val[now];
        else if(k <= siz[tree[now][0]]) return kth(k, tree[now][0]);
        else return kth(k - siz[tree[now][0]] - 1, tree[now][1]);
    }
4.查询k的排名

直接以 k1 k − 1 为关键字分离子树, 排名即为 siz[x]+1 s i z [ x ] + 1

case 3:
            {
                split(root, b - 1, x, y);
                printf("%d\n", siz[x] + 1);
                root = merge(x, y);
                break;
            }
前驱后继

对于前驱, 将小于k部分的子树剖出来, 查询其最大的一个; 对于后继, 将大于k部分的子树剖出来, 查询其最小的一个。

case 5:
            {
                split(root, b - 1, x, y);
                printf("%d\n", val[kth(siz[x], x)]);
                root = merge(x, y);
                break;
            }
case 6:
            {
                split(root, b, x, y);
                printf("%d\n", val[kth(1, y)]);
                root = merge(x, y);
                break;
            }

接下来谈谈队列方面。

区间翻转问题。

我们直接将翻转区间分裂出来, 打上标记放回去就好了…比起Splay来不知道简单了多少倍…

IN void reverse(const int &lef, const int &rig)
    {
        split (root, rig + 1, a, b);
        split (a, lef, c, d);
        tag[d] ^= 1;
        root = merge(merge(c, d), b);
    }

可持久化也是fhq Treap的一大优势。 因为每次split 和 merge 的时候部分链的相同位置并没有改变, 这和主席树有很大相似之处(相比起来Splay每次伸展都会几乎完全打乱顺序), 所以我们可以仿照主席树的思路(博主很久很久之前写的), 对一部分节点进行复用, 就可以达到可持久化的目的。

代码相对于非持久化版本的fhq Treap有一点差异, 大家慢慢品味。

#include <cstdio>
#include <cctype>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#define R register
#define W while
#define IN inline
#define gc getchar()
#define MX 500005
static bool fu;
template <class T>
IN void in(T &x)
{
    fu = false; R char c = gc; x = 0;
    W (!isdigit(c))
    {if(c == '-') fu = true; c = gc;}
    W (isdigit(c))
    {x = (x << 1) + (x << 3), x += c - 48, c = gc;}
    if(fu) x = -x;
}
namespace fhqTreap
{
    struct Node
    {
        int son[2], val, siz, pri;
    }tree[MX * 50];
    int root[MX];
    int cnt;
    IN int randomm()
    {
        static int seed = 703; 
        return seed = int(seed * 48271LL % 2147483647);
    }
    IN void pushup(const int &now)
    {tree[now].siz = tree[tree[now].son[0]].siz + tree[tree[now].son[1]].siz + 1;}
    IN int new_node (const int &giv)
    {
        tree[++cnt].val = giv;
        tree[cnt].siz = 1;
        tree[cnt].pri = randomm();
        return cnt;
    }
    int merge (int x, int y)
    {
        if(!x || !y) return x + y;
        if(tree[x].pri > tree[y].pri)
        {
            int now = ++cnt; tree[now] = tree[x]; 
            tree[now].son[1] = merge(tree[now].son[1], y);
            pushup(now); return now;
        }
        else
        {
            int now = ++cnt; tree[now] = tree[y];
            tree[now].son[0] = merge(x, tree[now].son[0]);
            pushup(now); return now;
        }
    }
    void split(int now, int k, int &x, int &y)
    {
        if(!now) {x = y = 0; return;}
        if(tree[now].val <= k)
        {
            x = ++cnt; tree[x] = tree[now];
            split(tree[x].son[1], k, tree[x].son[1], y);
            pushup(x);
        }
        else
        {
            y = ++cnt; tree[y] = tree[now];
            split(tree[y].son[0], k, x, tree[y].son[0]);
            pushup(y);
        }
    }
    void Del(int &root, int tar)
    {
        int x = 0, y = 0, z = 0;
        split(root, tar, x, z);
        split(x, tar - 1, x, y);
        y = merge(tree[y].son[0], tree[y].son[1]);
        root = merge(merge(x, y), z);
    }
    void insert(int &root, int tar)
    {
        int x = 0, y = 0, z = 0;
        split(root, tar, x, y);
        root = merge(merge(x, new_node(tar)), y);
    }
    int get_val(int now, int tar)
    {
        if(tar == tree[tree[now].son[0]].siz + 1) return tree[now].val;
        else if(tar <= tree[tree[now].son[0]].siz) return get_val(tree[now].son[0], tar);
        else return get_val(tree[now].son[1], tar - tree[tree[now].son[0]].siz - 1);
    }
    IN int kth(int &root, int tar)
    {
        int x, y;
        split(root, tar - 1, x, y);
        int ans = tree[x].siz + 1;
        root = merge(x, y);
        return ans;
    }
    IN int pre(int &root, int tar)
    {
        int x, y, k, ans;
        split(root, tar - 1, x, y);
        if(!x) return -2147483647;
        k = tree[x].siz;
        ans = get_val(x, k);
        root = merge(x, y);
        return ans;
    }
    IN int suc(int &root, int tar)
    {
        int x, y, ans;
        split(root, tar, x, y);
        if(!y) return 2147483647;
        else ans = get_val(y, 1);
        root = merge(x, y);
        return ans;
    }
}
using namespace fhqTreap;
int main(void)
{
    int q;
    R int base, com, t;
    in(q);
    for (R int i = 1; i <= q; ++i)
    {
        in(base), in(com), in(t);
        root[i] = root[base];
        switch(com)
        {
            case 1:
            {insert(root[i], t); break;}
            case 2:
            {Del(root[i], t); break;}
            case 3:
            {printf("%d\n", kth(root[i], t)); break;}
            case 4:
            {printf("%d\n", get_val(root[i], t)); break;}
            case 5:
            {printf("%d\n", pre(root[i], t)); break;}
            case 6:
            {printf("%d\n", suc(root[i], t)); break;}
        }
    }
}
<think>好的,用户想了解可持久化Treap的数据结构及实现方法,特别是代码部分。首先,我需要回顾一下可持久化数据结构的基本概念。可持久化数据结构能够保存历史版本,每次修改后生成新版本,而旧版本保持不变。常见的应用包括数据库版本管理、撤销操作等。 Treap本身是二叉搜索树和堆的结合,通过随机优先级维持平衡。非旋Treap,比如FHQ Treap,使用Split和Merge操作来避免转,这样更适合可持久化,因为转操作会改变结构,难以维护历史版本。 接下来,我需要参考用户提供的引用内容。引用3提到了非旋Treap可持久化的实现,特别是在Merge操作时有时不需要复制节点,这能提高效率。引用4和5讨论了Split和Merge的具体实现,以及如何查找第k大元素。引用1和2说明了可持久化的应用场景和限制,比如势能分析的数据结构不适合完全可持久化,但Treap通过非旋转操作可以实现。 现在需要将FHQ Treap可持久化方法分解为关键操作:Split和Merge的可持久化版本。每次修改时复制路径上的节点,保持旧版本的结构不变。例如,在Split时,如果当前节点需要被分割,就创建新节点复制原节点的内容,然后递归处理左右子树。Merge操作类似,根据优先级决定合并方式,必要时复制节点。 代码实现部分,节点结构需要左右子节点、大小和优先级。Split函数返回分割后的两个根节点,Merge函数合并两个Treap。插入和删除操作通过Split和Merge的组合实现,每次操作生成新版本。需要注意在修改路径上的节点时创建新副本,避免影响旧版本。 可能的问题点:确保每次操作只复制必要的节点,以节省空间。例如,在Merge时,如果子树来自同一版本,可能不需要复制。此外,路径复制可能导致额外的内存消耗,需要合理管理节点的创建和更新。 最后,需要验证示例代码的正确性,比如插入操作是否正确分割和合并,并生成新版本。测试用例可以帮助确认可持久化功能是否正常,例如插入元素后检查旧版本是否存在,新版本是否正确。</think>### 可持久化Treap(FHQ Treap)数据结构及实现方法 #### 一、核心设计原理 可持久化Treap通过**路径复制**实现版本管理,每次插入/删除操作时,仅复制涉及修改的节点路径,未修改的子树直接复用。基于非旋转的FHQ Treap实现,核心操作包括: 1. **带复制的分裂(Split)** 递归复制路径节点,返回两个新子树 $$ \text{Split}(root, key) \rightarrow (left\_tree, right\_tree) $$ 2. **带复制的合并(Merge)** 根据优先级合并子树,复用未修改部分 $$ \text{Merge}(x, y) \rightarrow new\_root $$ #### 二、代码实现(C++示例) ```cpp struct Node { int val, prio, size; Node *ch[2]; // ch[0]左子树, ch[1]右子树 Node(int v) : val(v), prio(rand()), size(1) { ch[0] = ch[1] = nullptr; } Node(const Node& o) { // 复制构造函数 val = o.val; prio = o.prio; size = o.size; ch[0] = o.ch[0]; ch[1] = o.ch[1]; } }; // 分裂操作(返回新版本的两棵子树) pair<Node*, Node*> Split(Node* p, int key) { if (!p) return {nullptr, nullptr}; Node* new_p = new Node(*p); // 关键复制操作[^3] if (key < new_p->val) { auto [left, right] = Split(new_p->ch[0], key); new_p->ch[0] = right; update_size(new_p); // 更新子树大小 return {left, new_p}; } else { auto [left, right] = Split(new_p->ch[1], key); new_p->ch[1] = left; update_size(new_p); return {new_p, right}; } } // 合并操作(生成新版本根节点) Node* Merge(Node* x, Node* y) { if (!x) return y; if (!y) return x; if (x->prio < y->prio) { Node* new_x = new Node(*x); // 复制节点[^5] new_x->ch[1] = Merge(new_x->ch[1], y); update_size(new_x); return new_x; } else { Node* new_y = new Node(*y); new_y->ch[0] = Merge(x, new_y->ch[0]); update_size(new_y); return new_y; } } // 插入操作(返回新版本根节点) Node* Insert(Node* root, int val) { auto [L, R] = Split(root, val); Node* new_node = new Node(val); return Merge(Merge(L, new_node), R); } ``` #### 三、关键优化点 1. **延迟复制机制**:仅在Split/Merge路径上复制节点,未修改子树直接复用[^3] 2. **版本共享策略**:新旧版本共享未修改的子树结构,空间复杂度为$O(m \log n)$(m为操作次数) 3. **平衡性保障**:通过随机优先级维持树高期望$O(\log n)$[^5] #### 四、应用场景 - 数据库事务版本管理(支持回滚操作) - 文本编辑器中的无限撤销/重做功能 - 函数式编程中的持久化集合[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值