「笔记」可持久化 Trie
写在前面
Q:什么时候需要可持久化?
A:想建一车 Trie 但是建不下的时候。
最好学习过可持久化线段树:Link。
介绍
当使用 Trie 维护整数的二进制拆分时,可以发现每次插入一个数时,最多仅会访问并新建到 \(\log w\) 个节点。换言之,新的 Trie 与原 Trie 仅有 \(\log w\) 个节点不同。
考虑维护多个版本指针,每次插入时新建被访问到的 \(\log w\) 个节点,继承部分原树信息,并将新版本指针指向新的根。构建的时间复杂度为 \(O(n\log n)\)。空间复杂度为 \(O(n\log^2 n)\)。
可持久化 Trie 可以将全局的二进制运算问题转化到区间上进行。
模板
给定一初始长度为 \(n\) 的非负整数序列 \(a\),给定 \(m\) 次操作:
- 在序列末尾添加一给定的数 \(x\),序列长度 \(n+1\)。
- 给定参数 \(l,r,x\),需要找到一个位置 \(p\),满足 \(l \le p \le r\),使得: \(\left(\bigoplus\limits_{i=p}^{n} a_i\right)\oplus x\)最大,输出最大是多少。
其中 \(\oplus\) 表示按位异或运算。\(1\le n,m\le 3\times 10^5\),\(1\le a_i\le 10^7\)。
1.5S,512MB。
看到区间异或,先转成前缀异或和形式,设 \(s_x = \bigoplus_{i=1}^{x} a_x\),则有:
\(s_n \oplus x\) 是定值,问题变为找到一个 \(p \in[l-1,r-1]\),使得 \(s_p\) 异或上 \(s_n \oplus x\) 最大。
一种暴力的想法是取出所有 \(s_i (i\in [l-1,r-1])\) 并用它们构成一棵 Trie,并在 Trie 上进行异或贪心。但时空均不允许我们暴力建 Trie,考虑可持久化。
具体地,进行所有插入操作时都新建历史版本,Trie 的每个节点都维护其子树中最晚插入的数的编号。查询时在第 \(r-1\) 个版本的 Trie 上贪心,这可以保证访问到的数的编号不大于 \(r-1\)。
贪心时仅转移到子树中最晚插入的数的编号不小于 \(l-1\) 的节点,保证深入到叶节点得到的数的编号不小于 \(l-1\)。
总复杂度 \(O((n +m)\log w)\) 级别,其中 \(w = \max a_i\)。
代码
//知识点:
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 6e6 + 10;
//=============================================================
int n, m, a[kN], sum[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace Trie {
#define ls (tr[now_][0])
#define rs (tr[now_][1])
const int kMaxRoot = kN;
const int kMaxNode = kN << 2;
int node_num, root[kMaxRoot], tr[kMaxNode][2], last[kMaxNode];
void Insert(int &now_, int pre_, int val_, int lth_, int id_) {
now_ = ++ node_num;
ls = tr[pre_][0], rs = tr[pre_][1], last[now_] = id_;
if (lth_ < 0) return ;
int ch = val_ >> lth_ & 1;
Insert(tr[now_][ch], tr[pre_][ch], val_, lth_ - 1, id_);
}
int Query(int now_, int val_, int lth_, int l_) {
if (lth_ < 0) return sum[last[now_]];
int ch = val_ >> lth_ & 1;
if (last[tr[now_][ch ^ 1]] >= l_) {
return Query(tr[now_][ch ^ 1], val_, lth_ - 1, l_);
}
return Query(tr[now_][ch], val_, lth_ - 1, l_);
}
}
void Init() {
n = read(), m = read();
Trie::last[0] = -1;
Trie::Insert(Trie::root[0], 0, 0, 25, 0);
for (int i = 1; i <= n; ++ i) {
sum[i] = sum[i - 1] ^ read();
Trie::Insert(Trie::root[i], Trie::root[i - 1], sum[i], 25, i);
}
}
//=============================================================
int main() {
Init();
while (m --) {
char opt[5]; scanf("%s", opt + 1);
if (opt[1] == 'A') {
++ n;
sum[n] = sum[n - 1] ^ read();
Trie::Insert(Trie::root[n], Trie::root[n - 1], sum[n], 25, n);
} else {
int l = read(), r = read(), val = sum[n] ^ read();
printf("%d\n", Trie::Query(Trie::root[r - 1], val, 25, l - 1) ^ val);
}
}
return 0;
}