可持久化 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:

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

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

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


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



// 在版本 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;
}
}
可以自由转载

浙公网安备 33010602011771号