可持久化平衡树详解及实现方法分析

目录

  • 前置要求
  • 核心思想
  • 实现方法比较:指针 与 数组模拟指针
  • Merge 是否应该新建节点
  • 参考程序

前置要求

带旋转的平衡树会改变祖先关系,这令可持久化变得困难。所以需要使用非旋的平衡树,如非旋treap。本文以非旋treap为例。

核心思想

可持久化的数据结构,其核心都是不改变历史的信息。当需要对信息进行修改的时候就新开一个节点,继承历史信息,然后再进行修改。

对于非旋treap来说,主要是对 Split 和 Merge 两个操作进行可持久化。剩下的操作不会对数据产生影响。而考虑到非旋treap上的操作在 Split 后一定跟随着对应的 Merge ,所以不需要对每个 Split 和 Merge 操作都进行可持久化。只要实现 Insert 和 Delete 的可持久化即可。

Split

Split 操作的可持久化比较容易实现,直接复制节点即可:

std::pair<int, int> Split(int Root, int Key) {
    if (!Root) return std::pair<int, int>(0, 0);
    std::pair<int, int> Temp;
    int New = ++Used;
    Pool[New] = Pool[Root];
    if (Key < Pool[New].Value) {
        Temp = Split(Pool[New].LeftChild, Key);
        Pool[New].LeftChild = Temp.second;
        Pool[New].Update();
        Temp.second = New;
    } else {
        Temp = Split(Pool[New].RightChild, Key);
        Pool[New].RightChild = Temp.first;
        Pool[New].Update();
        Temp.first = New;
    }
    return Temp;
}

Merge

Merge 操作过程中是否需要新建节点颇有争议。这一点会在下面 Merge 是否应该新建节点 中做详细的分析。在这里先给上新建节点的方式,毕竟这样一定不会错:

int Merge(int x, int y) {
    if ((!x) || (!y)) return x ^ y;
    int New = ++Used;
    if (Pool[x].Priority <= Pool[y].Priority) {
        Pool[New] = Pool[x];
        Pool[New].RightChild = Merge(Pool[New].RightChild, y);
        Pool[New].Update();
    } else {
        Pool[New] = Pool[y];
        Pool[New].LeftChild = Merge(x, Pool[New].LeftChild);
        Pool[New].Update();
    }
    return New;
}

这两个操作是整个可持久化平衡树的核心。在这两个操作的基础上,可以实现其他的操作,例如 Insert :

void Insert(int History, int Version, int Key) {
    Versions[Version] = Versions[History]; //先从历史版本复制信息。
    std::pair<int, int> Temp = Split(Versions[Version], Key); //将树分为不超过 Key 和大于 Key 两部分
    int New = ++Used; Pool[New] = node(Key); //新建节点
    Temp.first = Merge(Temp.first, New);
    Versions[Version] = Merge(Temp.first, Temp.second); //依次合并
    return;
}

在理解的基础上,你就可以解决这道题了:

luogu 3835

实现方法比较:指针 与 数组模拟指针

脱开这道题,指针的速度和数组的速度应当是相差不大的。在 Ubuntu18.04 系统, g++ 7.4.0 版本上,两个范围的测试结果如下(使用 chrono 库计时):

数组大小 1000, 进行 10000000 次访问及赋值,30 次测试取平均值。下标访问为 22ms ,指针访问为 25ms 。

数组大小 10000,进行 100000000 次访问及赋值,30 次测试取平均值。下标访问为 277ms ,指针访问为 263ms 。

可能系统和编译器对测试结果有影响,但可见两种方式其实差别不大。

但是在这道题目里,数组实现明显优于指针实现,原因有如下几点:

  • 虽然指针实现不需要对数组初始化,但是每次 new 一个节点也需要时间。new 操作不会快于数组的初始化;
  • 指针实现需要更多的对空指针的判断,这会耗去许多时间。而数组实现只需要将其赋值为 0 即可;
  • 在程序结束时,数组申请的连续的空间,所以销毁速度极快。而指针申请的空间不一定是连续的,所以销毁速度慢于数组。同样的,如果通过 --fsanitize=address 来分析内存,它会告诉你最后内存有溢出。
  • 指针实现不易于调试。

下面的分析基于上面那题。对于随机的极限数据,指针需要 26ms ,而数组只要 19ms (1000组数据取平均),指针耗时是数组的 1.36 倍。

而同样神奇的一点是在luogu上,相同的算法,指针消耗的空间远大于数组(不清楚原因)?同时指针速度远远慢于数组。(差了近两倍?)

有dalao指出,在某些平台上,指针消耗的时空都是数组的两倍

可以比较这两发及提交: 指针数组

所以还是建议写数组吧。只要在不MLE的情况下把数组往大开就好了。

Merge 是否应该新建节点

下面来仔细分析一下 Merge 的时候是否应该新建节点。

上面提到了, Split 和 Merge 可以看做一次操作。那么如果 Merge 中需要改变的节点都是 Split 中新建的节点,那么 Merge 就不需要新建节点。

实际上,这和维护的信息有关。如果将相同的关键字维护成不同的节点,那么 Merge 的时候就需要新建节点。考虑到删除的时候会分出来一棵权值都为关键字的树,而这棵树只有通向最左节点的那条链和最右节点的那两条链是新的节点,其他节点都是历史中的节点。如果是通过 Merge(LeftChild, RightChild) 来实现,就会访问到从根到叶子的随机一条路径(由于随机的优先级)。显然这条路径不一定是 Split 中新建的节点。或者,你可以手动删除最左边或最右边的节点。而这样实现并不那么方便与自然。

而如果将有相同关键字的信息维护成相同的点,那么 Merge 时就不需要新建节点。由于真正要改变的节点至多只有一个,而这个节点到根的路径都是 Split 时新建的节点,所以可以直接合并,不新建节点。

显然后一种做法更省空间。(前一种需要433MB,而后一种只需要225MB,2019.11.9luogu数据。)

luogu的数据较为宽松,上边错误的做法(也就是相同的关键字维护成不同的节点,但 Merge 的时候没有新建节点)也可以通过(见提交记录)。不过已经联系管理员加强数据了。(2019.11.9)或者你可以通过构造一些操作数有大量相同的数据来验证(如有 1000 次操作,类型随机,操作数 rand() %3+1,稍微拍一会儿就能够hack掉了)。

参考程序

这份代码用数组实现,并且将关键字相同的数据记在同一个节点。

#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <algorithm>
#define INF 2147483647
#define Maxn 500010
#define MaxSize 30000000

struct node {
    int Value, Priority, Size, Count;
    int LeftChild, RightChild;
    node();
    node(int _Value);
    inline void Update();
};
node Pool[MaxSize];
int Versions[Maxn], Used;

std::pair<int, int> Split(int Root, int Key);
int Merge(int x, int y);
int Find(int Root, int Key);
int Update(int Root, int Key, int State);
void Insert(int Version, int Key);
void Delete(int Version, int Key);
inline int Rank(int Root, int Key);
inline int Query(int Root, int Key);
inline int Precursor(int Root, int Key);
inline int Successor(int Root, int Key);

int n, Version, Opt, Key;

int main() {
    srand(time(NULL));
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d%d%d", &Version, &Opt, &Key);
        Versions[i] = Versions[Version];
        if (Opt == 1) Insert(i, Key);
        else if (Opt == 2) Delete(i, Key);
        else if (Opt == 3) printf("%d\n", Rank(Versions[i], Key));
        else if (Opt == 4) printf("%d\n", Query(Versions[i], Key));
        else if (Opt == 5) printf("%d\n", Precursor(Versions[i], Key));
        else printf("%d\n", Successor(Versions[i], Key));
    }
    return 0;
}

node::node() { return; }

node::node(int _Value) {
    Value = _Value;
    Priority = rand();
    Size = Count = 1;
    LeftChild = RightChild = 0;
    return;
}

inline void node::Update() {
    Size = Count + Pool[LeftChild].Size + Pool[RightChild].Size;
    return;
}

std::pair<int, int> Split(int Root, int Key) {
    if (!Root) return std::pair<int, int>(0, 0);
    std::pair<int, int> Temp;
    int New = ++Used;
    Pool[New] = Pool[Root];
    if (Key < Pool[New].Value) {
        Temp = Split(Pool[New].LeftChild, Key);
        Pool[New].LeftChild = Temp.second;
        Pool[New].Update();
        Temp.second = New;
    } else {
        Temp = Split(Pool[New].RightChild, Key);
        Pool[New].RightChild = Temp.first;
        Pool[New].Update();
        Temp.first = New;
    }
    return Temp;
}

int Merge(int x, int y) {
    if ((!x) || (!y)) return x ^ y;
    if (Pool[x].Priority <= Pool[y].Priority) {
        Pool[x].RightChild = Merge(Pool[x].RightChild, y);
        Pool[x].Update();
        return x;
    } else {
        Pool[y].LeftChild = Merge(x, Pool[y].LeftChild);
        Pool[y].Update();
        return y;
    }
}

int Find(int Root, int Key) {
    while (Root) {
        if (Pool[Root].Value == Key) return Root;
        if (Key < Pool[Root].Value) Root = Pool[Root].LeftChild;
        else Root = Pool[Root].RightChild;
    }
    return Root;
}

int Update(int Root, int Key, int State) {
    if (!Root) return 0;
    int New = ++Used; Pool[New] = Pool[Root];
    Pool[New].Size += State;
    if (Pool[New].Value == Key) {
        Pool[New].Count += State;
        return New;
    }
    if (Key < Pool[New].Value) Pool[New].LeftChild = Update(Pool[New].LeftChild, Key, State);
    else Pool[New].RightChild = Update(Pool[New].RightChild, Key, State);
    return New;
}

void Insert(int Version, int Key) {
    if (Find(Versions[Version], Key)) {
        Versions[Version] = Update(Versions[Version], Key, 1);
        return;
    }
    std::pair<int, int> Temp = Split(Versions[Version], Key);
    int New = ++Used; Pool[New] = node(Key);
    Temp.first = Merge(Temp.first, New);
    Versions[Version] = Merge(Temp.first, Temp.second);
    return;
}

void Delete(int Version, int Key) {
    int Temp = Find(Versions[Version], Key);
    if (!Temp) return;
    if (Pool[Temp].Count > 1) {
        Versions[Version] = Update(Versions[Version], Key, -1);
        return;
    }
    std::pair<int, int> Temp1 = Split(Versions[Version], Key);
    std::pair<int, int> Temp2 = Split(Temp1.first, Key - 1);
    Versions[Version] = Merge(Temp2.first, Temp1.second);
    return;
}

inline int Rank(int Root, int Key) {
    static int Ans; Ans = 0;
    while (Root) {
        if (Pool[Root].Value == Key) return Ans + Pool[Pool[Root].LeftChild].Size + 1;
        if (Key < Pool[Root].Value) Root = Pool[Root].LeftChild;
        else Ans += Pool[Pool[Root].LeftChild].Size + Pool[Root].Count, Root = Pool[Root].RightChild;
    }
    return Ans + 1;
}

inline int Query(int Root, int Key) {
    while (Root) {
        if (Pool[Pool[Root].LeftChild].Size < Key && Pool[Pool[Root].LeftChild].Size + Pool[Root].Count >= Key) return Pool[Root].Value;
        if (Pool[Pool[Root].LeftChild].Size >= Key) Root = Pool[Root].LeftChild;
        else Key -= Pool[Pool[Root].LeftChild].Size + Pool[Root].Count, Root = Pool[Root].RightChild;
    }
    return 0;
}

inline int Precursor(int Root, int Key) {
    static int Ans; Ans = -INF;
    while (Root) {
        if (Pool[Root].Value < Key) Ans = Pool[Root].Value, Root = Pool[Root].RightChild;
        else Root = Pool[Root].LeftChild;
    }
    return Ans;
}

inline int Successor(int Root, int Key) {
    static int Ans; Ans = INF;
    while (Root) {
        if (Pool[Root].Value > Key) Ans = Pool[Root].Value, Root = Pool[Root].LeftChild;
        else Root = Pool[Root].RightChild;
    }
    return Ans;
}

原文地址:https://www.cnblogs.com/chy-2003/p/11825302.html

时间: 2024-11-06 21:55:58

可持久化平衡树详解及实现方法分析的相关文章

linux rm 命令详解及使用方法实战【初级】

rm:删除命令 前言: windows中的删除命令大家都不陌生,linux中的删除命令和windows中有一个共同特点,那就是危险,前两篇linux mkdir 命令详解及使用方法实战[初级]中我们就提到了rm命令,现在我们来详细介绍一下linux中删除文件和目录的命令, rm命令,rm是常用的命令,该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除.对于链接文件,只是删除了链接,原有文件均保持不变. 名称 rm - remove files o

linux pwd 命令详解及使用方法实战【初级】

pwd:查看当前工作目录 前言: Linux中用 pwd 命令来查看”当前工作目录“的完整路径,就是经常提及的所在目录,多用在生产环境多级目录中查看当前所在路径,使用此命令能给运维人员/操作人员带来很多方便,当你不确定在什么目录下时即可直接键入此命令来进行“定位”. 上次分享的mkdir 命令详解及使用方法实战[初级]中提到过pwd当时只是简单的概括了一句,此处给大家详细讲解一下pwd使用方法,从而在将来的生产环境中更好的驾驭linux操作系统. 名称 pwd-print name of cur

linux mkdir 命令详解及使用方法实战【初级】

mkdir命令详解及使用方法实战 名称 MKDIR 是 make directories 的缩写 使用方法 mkdir [选项(如-p)] ...目录名称(及子目录注意用分隔符隔开)...    如使用mkdir建立一个多级目录则要使用-P命令 如: mkdir /lcp 是在根(/)目录下建立一个名为lcp的文件夹 mkdir -p /lcp/abc 则是在根(/)目录下建一个名为lcp的文件夹同时在此文件夹中建立名为abc的文件夹 描述 创建目录(IES),如果他们不存在. 长选项必须用的参

ListView使用详解,listActivity使用方法

OPhone开发中经常会用到各种各样的组件,像TextView,Button等等.其中经常会使用到ListView(列表),ListView以列表的形式展示具体内容,并且能够根据数据的长度自适应显示.本篇将由浅入深的介绍几种列表,并着重介绍如何自定义列表.具体的表现形式如图1所示.在OPhone系统中,列表的显示需要三个元素: 1.ListVeiw 用来展示列表的View. 2.适配器 用来把数据映射到ListView上的中介. 3.数据    具体的将被映射的字符串,图片,或者基本组件. 根据

FluentData 轻量级.NET ORM持久化技术详解

FluentData 轻量级.NET ORM持久化技术详解(C#4.5/Linq/Lambda,事务/储存过程,开源框架) 课程讲师:风舞烟 课程分类:.NET框架 适合人群:初级 课时数量:19课时 用到技术:FluentData 涉及项目:FluentData轻量级 咨询qq:1840215592 本课程系<基于ASP.NET MVC 4 +Knockout.JS+Web API+FluentData+EasyUI 技术实现Web通用商业开发框架>中的一个单品课程,北风产品总监"

AIX下PVID详解及其修改方法

AIX 下 PVID 详解及其修改方法 1.PVID 是什么 PVID 全称 physical volume identifier,它非常重要,相当于软序列号,当把一个磁盘变成 PV 时, 就生成了 PVID , PVID 是由机器序列号 (uname -m的前 8位 ) 和它生成的时间组成,这种机制保证了 PVID 的唯一性.当系统启动时,磁盘配置程序会寻找磁 盘上的 PVID ,并把它跟 ODM 库中的 PVID 信息进行比 对,如果在 ODM 中找到匹配的条目,则把和这个 PVID 相对应

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

SpringMVC视图机制详解[附带源码分析]

目录 前言 重要接口和类介绍 源码分析 编码自定义的ViewResolver 总结 参考资料 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:http://www.cnblogs.com/fangjian0423/p/springMVC-introduction.html 本文将分析SpringMVC的视图这部分内容,让读者了解SpringMVC视图的设计原理. 重要接口和类介绍 1. View接口 视图基础接口,它的各种实现类是无

Android ViewGroup触摸屏事件派发机制详解与源码分析

PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbober] 该篇承接上一篇<Android View触摸屏事件派发机制详解与源码分析>,阅读本篇之前建议先阅读. 1 背景 还记得前一篇<Android View触摸屏事件派发机制详解与源码分析>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事