浅谈可持久化Trie与线段树的原理以及实现

引言

当我们需要保存一个数据结构不同时间的每个版本,最朴素的方法就是每个时间都创建一个独立的数据结构,单独储存。

但是这种方法不仅每次复制新的数据结构需要时间空间上也受不了储存这么多版本的数据结构。

然而有一种叫git的工具,可以维护工程代码的各个版本,而空间上也不至于十分爆炸。怎么做到呢?

答案是版本分支,即每次创建新的版本不完全复制老的数据结构,而是在老的数据结构上加入不同版本的分支。

下面以链表为例

graph LR
A-->B
B-->C
C-->D
D-->E
E-->F
F-->G
B-->Z[C_新版本]
Z-->Y[D_新版本]
Y-->E

新的版本是部分建立在老的版本之上的。不变的地方不变,有编号的地方就加入新版本的分支。

实现可持久化Trie

基于版本分支的思想,我们怎么建立一个可持久化Trie呢?

其次我们注意到在上面那张链表图中进入下一个节点的时候,每次都要判断有没有我们要进入的新版本的分支,十分麻烦。有没有方法可以保证我们向下找的节点全部是我们要的版本呢?

有方法,我们只需要记录修改过的Trie的关键路径就好了。

什么叫关键路径呢?

这里先假设我们只修改Trie上的一个节点。而所谓关键路径就是从Trie的根节点到修改节点的路径,我们只创建这路径上的节点,其余节点全部继承一个老版本的节点。

如图

graph TD
ROOT-.c.->c
ROOT-.m.->m
c--a-->ca
ca--t-->cat
m--a-->ma
ma--p-->map
ROOT_NEW--c-->c;
ROOT_NEW--m-->m_NEW
m_NEW--a-->ma_NEW
ma_NEW--p-->map
ma_NEW--r-->mar_NEW
mar_NEW--k-->mark_NEW

这是一个向有{cat,map}的Trie里插入mark的新单词的例子。

不难发现,在ROOT_NEW可以到达的节点构成的树中,凡是不在mark这个单词的路径上的节点统统用的是老版本树的节点。

代码实现

由于可持久化Trie不是我们的主题,代码就不放了

代码很简单,就不多做解释了

#include <iostream>
#include <string>
using namespace std; const int N = 1e1 + 128; int his[128];
int h; int to[N][26];
int p; void insert(string &s)
{
int old = p; //老树的节点
his[++h] = ++p;
int now = p; //新树的
for (auto i : s)
{
for (int j = 0; j < 26; j++)
if (i - 'A' != j) //非关键路径上的节点继承老树
to[now][j] = to[old][j];
to[now][i - 'A'] = ++p; //关键路径上的节点就新建
now = p;
old = to[now][i - 'A']; //老树也要跟下去
}
return;
} bool ask(int h, string &s) //询问某个版本的Trie里,是否有对应的单词
{
int now = his[h];
for (auto i : s)
{
if (to[now][i - 'A'] == 0)
return false;
now = to[now][i - 'A'];
}
return true;
} int main()
{
int opt;
while (cin >> opt)
{
if (opt == 1) //插入
{
string str;
cin >> str;
insert(str);
}
else
{
string str;
int h;
cin >> str >> h;
cout << ((ask(h, str)) ? 'Y' : 'N') << endl;
}
}
return 0;
}

可持久化线段树

终于到了我们的主题了。可持久化线段树顾名思义就是可持久化的线段树

存在的意义首先是满足部分线段树的要求,然后也能根据线段树的特性解决一部分可持久化Trie的弊端。

聪明的小伙伴可以发现在一个Trie中,我们要把一条完整的子链完全复制下来,如果我们老版本的Trie本来就是一条链,这种操作无异于把Trie重新复制一遍,还是相当慢,怎么办?

在维护一个Trie的时候,这种问题可能会让人头疼。但是线段树可以完全避免,因为线段树的树高是完全有限的[\(Log (n)\)级别]。

基本思路还是只记录修改过的关键路径,不在关键路径上的节点继承老版本的子树。

基本原理还是和可持久化Trie差不多,看图和代码基本也能理解了

graph TD
A([1->4]).->B([1->2])
A.->C([3->4])
B-->D([1])
B-->E([2])
C.->F([3])
C.->G([4])
H([1->4_new])-->B
H-->I([3->4_new])
I-->F
I-->J([4_new])

代码实现

有一点要注意的是,这个线段树的节点关系已经是一个有向图了,不能用满二叉树的性质去计算他的左右儿子。需要手动记录。

其实就是P3919 【模板】可持久化线段树 1(可持久化数组)的题解

#include <iostream>
using namespace std; const int N = 5e7 + 128; int num[N / 10]; int his[N / 10];
int h_ptr; int lc[N], rc[N], val[N];
int p; int n, m; void build(int u, int l, int r) //和常规的线段树建立差不多,就是要左右儿子不能用满二叉树性质算出来了,所以要手动存
{
if (l == r)
{
val[u] = num[l];
return;
}
lc[u] = ++p;
rc[u] = ++p;
int mid = (l + r) >> 1;
build(lc[u], l, mid);
build(rc[u], mid + 1, r);
} void fork_only(int h) //仅仅只复制一个历史版本
{
his[h_ptr++] = his[h];
} void fork_and_edit(int h, int addr, int val_) //复制一个带修改的版本为h的历史版本到最新版本中(把addr这里的数修改为val)
{
int old = his[h];
int now = ++p;
his[h_ptr++] = p;
int l = 1, r = n;
while (l < r)
{
int mid = (l + r) >> 1;
if (addr <= mid) //关键路径在左儿子
{
rc[now] = rc[old]; //所以右儿子直接继承
lc[now] = ++p; //新建一个左儿子
now = lc[now];
old = lc[old];
r = mid;
}
else
{
lc[now] = lc[old]; //反之继承左儿子
rc[now] = ++p;
now = rc[now];
old = rc[old];
l = mid + 1;
}
}
val[now] = val_;
return;
} int query(int u, int addr)
{
int l = 1, r = n;
while (l < r)
{
int mid = (l + r) >> 1;
if (addr <= mid)
u = lc[u], r = mid;
else
u = rc[u], l = mid + 1;
}
return val[u];
} int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> num[i];
his[h_ptr++] = ++p;
build(p, 1, n);
for (int i = 1; i <= m; i++)
{
int v, opt, loc;
cin >> v >> opt >> loc;
if (opt == 1)
{
int value;
cin >> value;
fork_and_edit(v, loc, value);
}
else
{
cout << query(his[v], loc) << endl;
fork_only(v);
}
}
return 0;
}

如果对代码有问题欢迎评论斧正。

浅谈可持久化Trie与线段树的原理以及实现(带图)的更多相关文章

  1. [数据结构]ODT(珂朵莉树)实现及其应用,带图

    [数据结构]ODT(珂朵莉树)实现及其应用,带图 本文只发布于博客园,其他地方若出现本文均是盗版 算法引入 需要一种这样的数据结构,需要支持区间的修改,区间不同值的分别操作. 一般的,我们会想到用线段 ...

  2. HDU - 2665 Kth number 主席树/可持久化权值线段树

    题意 给一个数列,一些询问,问$[l,r]$中第$K$大的元素是哪一个 题解: 写法很多,主席树是最常用的一种之一 除此之外有:划分树,莫队分块,平衡树等 主席树的定义其实挺模糊, 一般认为就是可持久 ...

  3. hdu 3303 Harmony Forever (线段树 + 抽屉原理)

    http://acm.hdu.edu.cn/showproblem.php?pid=3303 Harmony Forever Time Limit: 20000/10000 MS (Java/Othe ...

  4. BZOJ2120&2453数颜色——线段树套平衡树(treap)+set/带修改莫队

    题目描述 墨墨购买了一套N支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问.墨墨会像你发布如下指令: 1. Q L R代表询问你从第L支画笔到第R支画笔中共有几种不同颜色的画笔. 2 ...

  5. BZOJ 4127: Abs (树链剖分 线段树求区间绝对值之和 带区间加法)

    题意 给定一棵树,设计数据结构支持以下操作 1 u v d 表示将路径 (u,v) 加d(d>=0) 2 u v 表示询问路径 (u,v) 上点权绝对值的和 分析 绝对值之和不好处理,那么我们开 ...

  6. 浅谈Mybatis持久化框架在Spring、SSM、SpringBoot整合的演进及简化过程

    前言 最近开始了SpringBoot相关知识的学习,作为为目前比较流行.用的比较广的Spring框架,是每一个Java学习者及从业者都会接触到一个知识点.作为Spring框架项目,肯定少不了与数据库持 ...

  7. trie(字典树)原理及C++代码实现

    字典树,又称前缀树,是用于存储大量字符串或类似数据的数据结构. 它的原理是利用相同前缀来减少查询字符串的时间. 不同于BST把关键字保存在本结点中,TRIE可以想象成把关键字和下一个结点的指针绑定,事 ...

  8. 浅谈欧几里得算法求最大公约数(GCD)的原理及简单应用

    一.欧几里得算法及其证明 1.定义: 欧几里得算法又称辗转相除法,用于求两数的最大公约数,计算公式为GCD(a,b)=GCD(b,a%b): 2.证明: 设x为两整数a,b(a>=b)的最大公约 ...

  9. 浅谈远程登录时,ssh的加密原理

    SSH:Secure Shell,是一种网络安全协议,主要用于登录远程计算机的加密过程. 登录方式主要有两种: 1.基于用户密码的登录方式:   加密原理:   当服务器知道用户请求登录时,服务器会把 ...

随机推荐

  1. Java下如何保证多线程安全

    前言         可能有人会觉得,只要我写代码的时候不去开启其他线程,那么就不会有多线程的问题了.         然而事实并非如此,如果仅仅是一些简单的测试代码,确实代码都会顺序执行而不是并发执 ...

  2. r正则表达式

    /t 制表符. /n 新行. . 匹配任意字符. | 匹配表达式左边和右边的字符. 例如, "ab|bc" 匹配 "ab" 或者 "bc". ...

  3. Redis挂了,流量把数据库也打挂了,怎么办?

    你好呀,我是歪歪. 是这样的,前几天有个读者给我发消息,说面试的时候遇到一个场景题: 他说他当时,一时间竟然找不到回答问题的角度,感觉自己没有回答到点子上. 我仔细想了一下,确实是感到这个问题有一丝丝 ...

  4. 【源码】“@Value 注入不成功”引发的一系列骚操作

    目录 背景 模拟@Value成功的场景 模拟注入不成功的场景 看看为什么没有注入成功 为什么加static和不加static的加载顺序是不一样的呢 我们不加static,能不能也让它注入成功呢? 总结 ...

  5. 自动部署Springboot项目脚本小脚本

    #!/bin/bash echo '自动部署Springboot项目脚本...' # aaa.jar 项目jar包 pid=`ps -ef|grep aaa.jar|grep -v grep|grep ...

  6. Spring Boot从入门到精通(十一)集成Swagger框架,实现自动生成接口文档

    Swagger是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.Swagger 是一组开源项目,其中主要要项目如下: Swagger-tools:提供各种与S ...

  7. 解决ModuleNotFoundError: No module named 'pip'问题

    Python学习遇到小问题:ModuleNotFoundError: No module named 'pip' 今天想要装一下第三方库exifread的时候发现cmd窗口下无法执行pip命令,想了想 ...

  8. linux命令打基础

    目录 一.shell概述 二.linux命令分类 三.linux命令行 3.1 格式 3.2 编辑Linux命令行 四.Linux基础命令 4.1 pwd:查看当前的工作目录 4.2 cd:切换工作目 ...

  9. 黑盒渗透测试【转自HACK学习-FoxRoot】

    因 搜到一篇写渗透测试步骤比较详细的文章,转过来学习,方便时常看看. 内容如下: 一.信息搜集 主动/被动搜集 信息搜集分为主动信息搜集和被动信息搜集. 主动信息搜集就是通过直接访问和扫描信息的方式进 ...

  10. 走心的中级Android工程师跳槽经验分享

    这些经验是我最近四个月,从准备面试到找到合适工作的汗水和泪水,希望对你们能有帮助! define 跳槽 跳槽前要思考的问题 钱不到位怎么办 心委屈怎么办 离职前的思考 确定要走时需要做的准备 行情怎么 ...