Trie树的可持久化

前言

正常的trie树一般用来解决字符串问题,特殊的比如01字典树,可以用来解决最大异或对(传送门),这里不在过多赘述。今天介绍的是trie树的可持久化。


可持久化是什么?

持久化是将程序数据在持久状态和瞬时状态间转换的机制。通俗的讲,就是瞬时数据(比如内存中的数据,是不能永久保存的)持久化为持久数据(比如持久化至数据库中,能够长久保存)——百度百科

可持久化是一个重要的算法思想,我对可持久化的理解就是:我们需要历史版本的信息,可以支持回溯到某个历史版本,并且可以基于当前版本扩展出另一个新的版本。

通俗来讲,可持久化就是指一个数据结构能查询历史记录,我们需要借助该结构,查询过去版本中有用的信息。(思想类似于git,分布式版本控制系统)


为什么需要可持久化?

好比最大异或对那题,如果每次都是询问一个区间,Trie树就不好处理,因为不能对每个区间都新建一颗Trie树,那样空间开销太大。这时候就需要我们的可持久化Trie树。


可持久化Trie

优点:从某个版本开始,能够遍历到该版本内的所有节点

可以解决动态的异或问题

先来看个例子:

现有4个字符串:cat、rat、cab、frg

下图是这几个字符构成的Trie树
请添加图片描述


每次只修改被添加或值被修改的节点,而保留没有被改动的节点,在上一个版本的基础上连边,使最后每个版本的Trie树的根遍历所能分离出的Trie树都是完整且包含全部信息的。

构建可持久化Trie树的过程如下:

第一个版本:(cat)
请添加图片描述

第二个版本:(cat、rat)
请添加图片描述

第三个版本:(cat、rat、cab)
请添加图片描述

第四个版本:(cat、rat、cab、frg)

请添加图片描述

这样一颗持久化Trie就构建好了,对于每一个版本,都可以从该版本根节点出发,找到历史各个版本的信息。


例题

传送门:最大异或和


题目描述

对一个长度为 n {n} n 的数组 a {a} a 维护 m {m} m次以下操作:

  1. A x:添加操作,在数组的末尾添加一个数 x {x} x,数组的长度 n {n} n 自增 1 {1} 1
  2. Q l r x :询问操作,给出查询区间 [ l , r ] {[l,r]} [l,r] 和一个值 x {x} x,求当 l < = p < = r {l <= p <= r} l<=p<=r时,使得: a [ p ] ⊕ a [ p + 1 ] ⊕ . . . ⊕ a [ n ] ⊕ x {a[p]⊕ a[p+1]⊕ ... ⊕ a[n] ⊕x } a[p]a[p+1]...a[n]x 最大,输出最大值

思路

  1. 维护前缀异或和: s [ i ] = s [ 1 ] ⊕ s [ 2 ] ⊕ . . . ⊕ s [ i ] {s[i] = s[1]⊕s[2]⊕...⊕s[i]} s[i]=s[1]s[2]...s[i]

  2. 如果询问是 1 {1} 1 ~ r {r} r,那么答案就是 s [ p − 1 ] ⊕ s [ n ] ⊕ x {s[p - 1] ⊕ s[n] ⊕ x} s[p1]s[n]x

    • 那么问题就转化为了:两个数异或最大,即最大异或对问题
    • s [ n ] ⊕ x = C {s[n] ⊕ x}=C s[n]x=C,即找到区间中某数与C异或最大,trie树上贪心去找
    • 遍历到二进制某位:1就看0是否存在,0就看1是否存在
  3. 而询问是 l {l} l ~ r {r} r,这时就需要多维护一个信息 m a x {max} max_ i d {id} id

    • 问题转化为: 在第 r {r} r个版本之前去找,并且选择走 0 ∣ 1 {0|1} 01的路线的节点中,该节点中至少存在一个数,它的下标 ≥ L {≥L} L。也就等价于,下标最大值 ≥ L {≥L} L

    • m a x {max} max_ i d {id} id:表示当前节点所能表示的数的最大编号(数是 s [ i ] {s[i]} s[i]编号 i {i} i

    • 保证在trie树中找到的路线上每个节点都在区间范围内

最终结果: s [ p ] ⊕ C {s[p]⊕C} s[p]C l − 1 < = p < = r − 1 {l-1<=p<=r-1} l1<=p<=r1


代码

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

// 3e5原始,3e5操作,1e7开25位够
const int N = 6e5 + 10, M = 25 * N;

int n, m;
int s[N];
int tr[M][2], max_id[M];
int root[N], idx;

// i是第i个插入的数的i, p是上一个插入的数的节点号, q是当前节点号, k是现在取到第k位
// s[i] 是当前处理的数
void insert(int i, int k, int p, int q) {
    // 已经处理完了最后一位
    if (k < 0) { 
        // 当前q为叶节点,记录当前节点所能到达的最大范围i
        max_id[q] = i;
        return ;
    }
    // 取出当前要处理的数s[i]的第k位
    int v = s[i] >> k & 1; 
    
    // 如果前一个节点有当前节点没有的分支,指向过去,这就等价于拷贝了历史信息
    if (p) tr[q][v ^ 1] = tr[p][v ^ 1];
    
    // 现在才是正常的trie树插入
    
    // 当前新开一个节点
    tr[q][v] = ++idx;
    
    // 递归插入二进制的下一位
    // 之前已经复制了v的不同方向路径的信息(v:0 1 两个方向)
    // 现在v这条方向也要考虑
    // 如果p这条v路径存在(等价于图的虚线处),向下延迟,暂时不需要复制
    insert(i, k - 1, tr[p][v], tr[q][v]);
    
    // 向上回溯,每个点保存子节点最大范围的值
    max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}

// 非递归版本
// 发现max_id其实就是当前新加的节点在前缀和数组s的位置
void insert(int k, int p, int q) {
    max_id[q] = k;
    for (int i = 23; i >= 0; i--) {
        int v = s[k] >> i & 1;
        if(p) tr[q][v ^ 1] = tr[p][v ^ 1];
        tr[q][v] = ++idx;
        max_id[tr[q][v]] = k;
        q = tr[q][v], p = tr[p][v];
    }
}

// 在[l, r]区间中,找一值与C异或最大
int query(int l, int r, int C) {
    int p = root[r];
    
    // C是s[n] ^ x, 从高位到低位逐位检索二进制每一位上能跟C异或结果最大的数
    for (int i = 23; i >= 0; i--) {
        int v = C >> i & 1;
        
        // 0是空节点,默认max_id[0] = 0的话,当l==0,就会令p跳到空节点上来
        // 由于每个节点至少有一条s[i]的完整路径
        // 那么tr[p][v ^ 1]是空,则tr[p][v]一定存在路
        // 那必不能 满足if,去走一条没路的路
        // 所以max_id[0] = -1,能判除这类情况
        if (max_id[tr[p][v ^ 1]] >= l) p = tr[p][v ^ 1];
        else p = tr[p][v]; // 退而求其次
    }
    return C ^ s[max_id[p]];
}

int main()
{
    scanf("%d%d", &n, &m);
    
    // 前缀和,初始化第0个版本
    s[0] = 0;
    max_id[0] = -1;
    root[0] = ++ idx;
    // insert(0, 23, 0, root[0]);
    insert(0, 0, root[0]);
    
    for (int i = 1; i <= n; i++) {
        int x; scanf("%d", &x);
        s[i] = s[i - 1] ^ x;
        root[i] = ++ idx;
        // insert(i, 23, root[i - 1], root[i]);
        insert(i, root[i - 1], root[i]);
    }
    
    char op[2];
    int l, r, x;
    while(m--) {
        scanf("%s", op);
        if (op[0] == 'A') {
            scanf("%d", &x);
            n++;
            s[n] = s[n - 1] ^ x;
            root[n] = ++idx;
            // insert(n, 23, root[n - 1], root[n]);
            insert(n, root[n - 1], root[n]);
        } else{
            scanf("%d%d%d", &l, &r, &x);
            printf("%d\n", query(l - 1, r - 1, s[n] ^ x));
        }
    } 
    
    return 0;
}

2022年4月16日15点22分
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ღCauchyོꦿ࿐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值