參考資料
《淺談可追溯化數據結構》————孔朝哲 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) << '\n';
continue;
}
cout << ques(r-1, l-1, x ^ las) << '\n';
}
}
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 << '\n';
}
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)] << '\n';
}
return 0;
}
可持久化並查集加強版
acwing 的題面怎么這么神必啊, 建議看 luogu 的題面。
這題就是用可持久化數組實現可持久化並查集, 用可持久化線段樹實現可持久化數組。
這題不能用路徑壓縮, 因為路徑壓縮的復雜度是均攤的,可以構造數據不斷回到 對於某個操作需要高復雜度的版本,然后執行操作, 這樣就可以把復雜度卡到爆炸。
要用復雜度穩定的啟發式合並來做, 查詢穩定 \(O(\log^2 n)\), 修改穩定增加 \(O(log n)\) 空間。