参考资料
《浅谈可追溯化数据结构》————孔朝哲 2019中国国家候选队论文 草
《可持久化数据结构研究》————陈立杰
《算法竞赛进阶指南》———— 李煜东
感谢他们的文字。
前言
一个数据结构通过修改操作改变自身结构(也可能改变数据),就称 这个数据结构的版本得到了更新。
将一个数据结构可持久化, 就是利用共用一部分结构的思想, 在空间上高效地保存这个数据结构的 所有历史版本。
Trie 的可持久化及其应用
对 Trie 的插入可持久化, 首要的问题就是不能对上一个版本进行丝毫改变, 再就是确实地保存此版本的正确结构, 最后就是尽量与上个版本共用空间。
这里介绍一个实现可持久化 Trie 的算法。
算法流程
设要插入的字符串为 s, 下标从零开始。
1.设之前最新版本 Trie 的根为 root, 设 p = root , i = 0。
2.建立一个新节点 root' 作为更新版本的根, 设 q = root'
3.对于所有字符集里的字符 c, ch[q]->c = ch[p]->c
4.新建节点 h, ch[q]->c = h
5.p = ch[p]->s[i], q = ch[q]->s[i], i += 1;
6.重复 3~5 直到 i = len(s) 时终止算法。
正确性:
首先算法中没有对之前版本的 Trie 上的任何指针进行更改, 所以不会改变上一个版本的结构。
至于能不能确实地保存当前版本的 Trie, 我描述不出来, 证明待补。
但我觉得证明这个是有价值的, 或许还可以打开新世界的大门, 所以我一定会回来补证明的。
复杂度:
复杂度就显然了, 时间与空间复杂度都是 (O(插入串的总长))。
最大异或和
将后缀异或和转化为两个前缀异或和的异或和。
设 s[i] 表示直到 a[i] (包括 a[i])的前缀异或和。
每次查询就转化为:给定 l,r 找一个最大的 p ((l-1 le p le r-1)), 使得 s[p] xor s[n] xor x 最大。
如果 p 的范围只有 (le r-1) 的限制, 就可以直接可持久化 0/1 Trie 做了。
考虑给 Trie 的节点增加额外的信息, 使得不至于在查询的过程中走到 (< l-1) 的节点 : 在可持久化 Trie 中插入数的时候, 给新建的节点染色,这样, 如果一个节点的颜色是位置 (l-1<) 的数的颜色, 这说明以这个节点为根的子树内只有位置 (< l-1) 的数的终止节点, 在 Trie 中游走的时候避免走这类点, 就可以在满足 (le r-1) 限制的同时满足 (ge l-1) 的限制。
#include<bits/stdc++.h>
using namespace std;
const int N = 600003;
int n,m,las;
int tot, root[N], ch[N*24][2], col[N*24];
void insert(int id, int tmp) {
int p = root[id-1], q = root[id] = ++tot;
col[q] = id;
for(int i=23;i>=0;--i) {
int v = (tmp>>i)&1;
col[ch[q][v] = ++tot] = id;
ch[q][v^1] = ch[p][v^1];
q = ch[q][v];
p = ch[p][v];
}
}
int ques(int id, int underlim, int tmp) {
int res = 0;
int p = root[id];
for(int i=23;i>=0;--i) {
int v = (tmp>>i)&1;
if(ch[p][v^1] && col[ch[p][v^1]] >= underlim) res += (1<<i), p = ch[p][v^1];
else p = ch[p][v];
}
return res;
}
int main() {
scanf("%d%d", &n,&m);
for(int i=1, a; i<=n; ++i) {
scanf("%d", &a);
las = las ^ a;
insert(i, las);
}
char s[3];
int l,r,x;
while(m--)
{
scanf("%s", s);
if(s[0] == 'A')
{
scanf("%d", &x);
las = las ^ x;
insert(++n, las);
}
else
{
scanf("%d%d%d", &l, &r, &x);
if(r==1) {
cout << (las ^ x) << '
';
continue;
}
cout << ques(r-1, l-1, x ^ las) << '
';
}
}
return 0;
}
把连续异或和拆成两个前缀异或和的异或和, 问题就变成了区间内选两个点, 使得异或和尽量大。
考虑分块, 预处理两端点都在一段连续块之间的答案, 这样, 一个询问只要做两遍 最大异或和 里的做法就行了。
预处理的时候要用区间 DP, 预处理的时候也要用到 最大异或和 里的做法。
常数有 (30), 挺吓人的, 直到我看了数据范围之后。
#include<bits/stdc++.h>
using namespace std;
const int N = 12003;
const int M = 6003;
const int Mb = 111;
int tot, ch[N*41][2], col[N*41], root[N];
void insert(int id, int tmp) {
int p=root[id-1], q=root[id]=++tot;
col[q] = id;
for(int i=30;i>=0;--i) {
int v = (tmp>>i) & 1;
ch[q][v^1] = ch[p][v^1];
col[ch[q][v]=++tot] = id;
p=ch[p][v], q=ch[q][v];
}
}
int ask(int id, int underlim, int tmp) {
int p=root[id], res=0;
for(int i=30;i>=0;--i) {
int v = ((tmp>>i)&1) ^ 1;
if(col[ch[p][v]] and col[ch[p][v]] >= underlim) res |= (1<<i);
else v^=1;
p = ch[p][v];
}
return res;
}
int n,m,a[N];
int B, mxpos, pos[N], L[Mb], R[Mb], f[Mb][Mb];
void init() {
B = sqrt(n*1.0);
for(int i=1;i<=n;++i) pos[i] = (i-1)/B + 1;
mxpos = pos[n];
for(int i=1;i<=mxpos;++i) L[i]=(i-1)*B+1, R[i]=i*B;
R[mxpos] = min(R[mxpos], n);
for(int i=1;i<=mxpos;++i)
for(int r=L[i]; r<=R[i]; ++r)
for(int l=L[i]-1;l<r;++l)
f[i][i] = max(f[i][i], a[l]^a[r]);
for(int len=2;len<=mxpos;++len)
for(int l=1;l+len-1<=mxpos;++l) {
int r = l+len-1;
f[l][r] = f[l][r-1];
for(int i=L[r];i<=R[r];++i) f[l][r] = max(f[l][r], ask(i-1, L[l]-1, a[i]));
}
}
int main() {
scanf("%d%d", &n,&m);
for(int i=1;i<=n;++i) {
scanf("%d", &a[i]); a[i] ^= a[i-1]; insert(i,a[i]);
}
init();
int lastans = 0;
while(m--) {
int x,y,l,r;
scanf("%d%d", &x,&y);
l = ((long long)x+lastans)%n + 1;
r = ((long long)y+lastans)%n + 1;
if(l>r) swap(l,r);
lastans = 0;
if(pos[l]==pos[r]) {
for(int i=l;i<=r;++i)
for(int j=l-1;j<i;++j)
lastans = max(lastans, a[i]^a[j]);
} else {
lastans = f[pos[l]+1][pos[r]-1];
for(int i=L[pos[r]];i<=r;++i) lastans = max(lastans, ask(i-1,l-1,a[i]));
for(int i=l;i<=R[pos[l]];++i) lastans = max(lastans, ask(r,i,a[i-1]));
}
cout << lastans << '
';
}
return 0;
}
单点修改可持久化线段树及其应用
一般不考虑支持区间修改的可持久化线段树, 因为标记下传很麻烦, 如果用标记永久化, 局限性又很大。
实现可持久化线段树的算法和实现可持久化 Trie 的算法一模一样。
可持久化线段树的单次插入和查询时间复杂度都是 (O(log n)), 单次插入的空间复杂度是 (O(log n)) 。
静态区间第k大
在值域线段树上二分可以求值域的第 (k) 大, 把值域线段树可持久化,把序列从前往后依次插入可持久化值域线段树(其实就是把值域做了前缀和), 一段区间的值域线段树就变成了两个可持久化线段树的差。
另外, 将值域离散化虽然对时间和空间都只有常数级别的优化, 但优化也是很明显的。
#include<bits/stdc++.h>
using namespace std;
const int N = 100003;
const int M = 10003;
struct sgt{
int tot, ch[2000003][2], cnt[2000003], root[N];
void insert(int p, int &q, int l, int r, int x) {
q = ++tot;
ch[q][0]=ch[p][0], ch[q][1]=ch[p][1];
if(l==r) {cnt[q]=cnt[p]+1; return;}
int mid = (l+r)>>1;
if(x<=mid) insert(ch[p][0], ch[q][0], l, mid, x);
else insert(ch[p][1], ch[q][1], mid+1, r, x);
cnt[q] = cnt[ch[q][0]] + cnt[ch[q][1]];
}
int ask(int p, int q, int l, int r, int k) {
if(l==r) return l;
int mid = (l+r)>>1;
int lcnt = cnt[ch[q][0]] - cnt[ch[p][0]];
if(k<=lcnt) return ask(ch[p][0], ch[q][0], l, mid, k);
else return ask(ch[p][1], ch[q][1], mid+1, r, k-lcnt);
}
} T;
int n,m,a[N], b[N], row[N];
int main() {
scanf("%d%d", &n,&m);
for(int i=1;i<=n;++i) scanf("%d", &a[i]), b[i]=a[i];
sort(b+1,b+1+n);
for(int i=1;i<=n;++i) {
int to = lower_bound(b+1,b+1+n,a[i]) - b;
row[to] = a[i];
a[i] = to;
T.insert(T.root[i-1], T.root[i], 1, n, a[i]);
}
while(m--) {
int l,r,k;
scanf("%d%d%d", &l,&r,&k);
cout << row[T.ask(T.root[l-1], T.root[r], 1, n, k)] << '
';
}
return 0;
}
可持久化并查集加强版
acwing 的题面怎么这么神必啊, 建议看 luogu 的题面。
这题就是用可持久化数组实现可持久化并查集, 用可持久化线段树实现可持久化数组。
这题不能用路径压缩, 因为路径压缩的复杂度是均摊的,可以构造数据不断回到 对于某个操作需要高复杂度的版本,然后执行操作, 这样就可以把复杂度卡到爆炸。
要用复杂度稳定的启发式合并来做, 查询稳定 (O(log^2 n)), 修改稳定增加 (O(log n)) 空间。