可持久化 Trie 树

一、什么是可持久化数据结构

可持久化数据结构(Persistent Data Structure) 是指在对数据结构进行修改时,保留其历史版本的数据结构。换句话说,每次更新操作都不会修改原有版本,而是创建一个新的版本,同时尽可能与旧版本共享未修改的部分来节省空间。

可持久化 Trie 树就是支持查询历史版本的 Trie 树。

二、为什么需要可持久化 Trie 树

应用场景

  • 查询某个历史版本中的信息
  • 区间查询问题(如区间异或最大值)
  • 需要回溯操作的场景

经典问题

  • 给定一个序列,查询区间 \([l, r]\) 内与给定值 \(x\) 异或最大的数
  • 查询字符串在某个历史版本中出现的次数

三、基本思想

可持久化 Trie 的核心思想是路径复制(Path Copying)

  • 每次插入操作只修改从根到叶子的一条路径
  • 未修改的节点与旧版本共享
  • 修改路径上的每个节点都会被复制,形成新节点
  • 新版本的根节点指向新复制的节点

这样,每个版本只需额外存储 \(O(\text{路径长度})\) 的新节点,而不是整棵树。

四、具体实现(以二叉 Trie 为例)

数据结构定义:

struct nood{
    ll ch[2];
    ll cnt;// 经过该节点的数字个数
}tr[1000000];
ll root[1000000];// 每个版本的根节点
ll nodecnt;// 节点计数器
ll newnode(){
    ++nodecnt;
    tr[nodecnt].ch[0]=tr[nodecnt].ch[1]=0;
    tr[nodecnt].cnt=0;
    return nodecnt;
}

插入新节点:

这是一个重点,我们一步步来讲。

在这个例子中,我们要维护每个版本的 cnt

这是某一个版本的 Trie 树,我们要在此基础上往里面插入一个字符串 0111
1

我们先把指针放在原始版本的根上,并建一个新根:

2

我们的第一步要往 0 走,这意味着 1 方向的儿子及其子树(图中蓝色方框部分)的 cnt 不会修改,和原来的版本是一样的。

3

于是我们把新根的 1 的儿子连到原始版本的 1 的儿子上,并且新建一个 0 的节点,cnt 为原始版本该位置上的 cnt+1

4

5

我们按照这种策略一直做下去(走的方向新建点,不走的方向连回原始版本),就可以建出这个新的 Trie,插入操作就完成啦!

6

7

8

// 在版本 pre 的基础上插入数字 x,返回新版本的根
ll insert(ll pre,ll x){
    ll newRoot=newnode();
    ll cur=newRoot;
    tr[cur]=tr[pre];
    tr[cur].cnt=tr[pre].cnt+1;
    for(int i=26;i>=0;i--){
        ll bit=(x>>i)&1;
        // 不走的方向连回原树:
        tr[cur].ch[bit^1]=tr[pre].ch[bit^1];
        //走的方向新建节点:
        ll nxt=newnode();
        tr[cur].ch[bit]=nxt;
        cur=nxt;
        pre=tr[pre].ch[bit];
        // 复制旧节点的信息并更新计数
        if(pre){
            tr[cur]=tr[pre];
        }
        //记得加cnt
        tr[cur].cnt++;
    }
    return newRoot;
}

统计答案

那你可能要问了,我们统计这个 cnt 有何用啊?

简单,如果我们询问 \([L,R]\) 这个区间,我们仅需要从 \(L-1\)\(R\) 的根开始跑,仅需要判断 tr[tr[nowr].ch[fx]].cnt-tr[tr[nowl].ch[fx]].cnt 是不是大于零就可以知道能不能往下走了。

code

下面的代码示例是查询区间 \([l,r]\) 内与 \(x\) 异或最大的值:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
struct nood{
    ll ch[2];
    ll cnt;// 经过该节点的数字个数
}tr[1000000];
ll root[1000000];// 每个版本的根节点
ll nodecnt;// 节点计数器
ll newnode(){
    ++nodecnt;
    tr[nodecnt].ch[0]=tr[nodecnt].ch[1]=0;
    tr[nodecnt].cnt=0;
    return nodecnt;
}
// 在版本 pre 的基础上插入数字 x,返回新版本的根
ll insert(ll pre,ll x){
    ll newRoot=newnode();
    ll cur=newRoot;
    tr[cur]=tr[pre];
    tr[cur].cnt=tr[pre].cnt+1;
    for(int i=26;i>=0;i--){
        ll bit=(x>>i)&1;
        tr[cur].ch[bit^1]=tr[pre].ch[bit^1];
        ll nxt=newnode();
        tr[cur].ch[bit]=nxt;
        cur=nxt;
        pre=tr[pre].ch[bit];
        // 复制旧节点的信息并更新计数
        if(pre){
            tr[cur]=tr[pre];
        }
        tr[cur].cnt++;
    }
    return newRoot;
}
ll query(ll rl,ll rr,ll x){
    ll ans=0;
    ll nowl=rl,nowr=rr;
    for(int i=26;i>=0;i--){
        ll bit=(x>>i)&1;
        ll fx=bit^1;
        ll cntt=tr[tr[nowr].ch[fx]].cnt-tr[tr[nowl].ch[fx]].cnt;
        if(cntt>0){
            ans|=(1<<i);
            nowl=tr[nowl].ch[fx];
            nowr=tr[nowr].ch[fx];
        }
        else{
            nowl=tr[nowl].ch[bit];
            nowr=tr[nowr].ch[bit];
        }
    }
    return ans;
}
int main(){
    ll n,m;
    cin>>n>>m;
    root[0]=newnode();
    for(int i=1;i<=n;i++){
        ll x;
        cin>>x;
        root[i]=insert(root[i-1],x);
    }
    while(m--){
        ll l,r,x;
        cin>>l>>r>>x;
        cout<<query(root[l-1],root[r],x)<<endl;
    }
}
posted @ 2026-08-20 16:12  MistyPost  阅读(3)  评论(0)    收藏  举报