淺談簡單可持久化數據結構及其應用


參考資料

《淺談可追溯化數據結構》————孔朝哲 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;
}

Fotile模擬賽L

把連續異或和拆成兩個前綴異或和的異或和, 問題就變成了區間內選兩個點, 使得異或和盡量大。
考慮分塊, 預處理兩端點都在一段連續塊之間的答案, 這樣, 一個詢問只要做兩遍 最大異或和 里的做法就行了。
預處理的時候要用區間 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)\) 空間。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM