淺談可持久化Trie與線段樹的原理以及實現
引言
當我們需要保存一個數據結構不同時間的每個版本,最朴素的方法就是每個時間都創建一個獨立的數據結構,單獨儲存。
但是這種方法不僅每次復制新的數據結構需要時間,空間上也受不了儲存這么多版本的數據結構。
然而有一種叫git的工具,可以維護工程代碼的各個版本,而空間上也不至於十分爆炸。怎么做到呢?
答案是版本分支,即每次創建新的版本不完全復制老的數據結構,而是在老的數據結構上加入不同版本的分支。
下面以鏈表為例
新的版本是部分建立在老的版本之上的。不變的地方不變,有編號的地方就加入新版本的分支。
實現可持久化Trie
基於版本分支的思想,我們怎么建立一個可持久化Trie呢?
其次我們注意到在上面那張鏈表圖中進入下一個節點的時候,每次都要判斷有沒有我們要進入的新版本的分支,十分麻煩。有沒有方法可以保證我們向下找的節點全部是我們要的版本呢?
有方法,我們只需要記錄修改過的Trie的關鍵路徑就好了。
什么叫關鍵路徑呢?
這里先假設我們只修改Trie上的一個節點。而所謂關鍵路徑就是從Trie的根節點到修改節點的路徑,我們只創建這路徑上的節點,其余節點全部繼承一個老版本的節點。
如圖
這是一個向有{cat,map}的Trie里插入mark的新單詞的例子。
不難發現,在ROOT_NEW可以到達的節點構成的樹中,凡是不在mark這個單詞的路徑上的節點統統用的是老版本樹的節點。
代碼實現
由於可持久化Trie不是我們的主題,代碼就不放了
代碼很簡單,就不多做解釋了
#include <iostream>
#include <string>
using namespace std;
const int N = 1e1 + 128;
int his[128];
int h;
int to[N][26];
int p;
void insert(string &s)
{
int old = p; //老樹的節點
his[++h] = ++p;
int now = p; //新樹的
for (auto i : s)
{
for (int j = 0; j < 26; j++)
if (i - 'A' != j) //非關鍵路徑上的節點繼承老樹
to[now][j] = to[old][j];
to[now][i - 'A'] = ++p; //關鍵路徑上的節點就新建
now = p;
old = to[now][i - 'A']; //老樹也要跟下去
}
return;
}
bool ask(int h, string &s) //詢問某個版本的Trie里,是否有對應的單詞
{
int now = his[h];
for (auto i : s)
{
if (to[now][i - 'A'] == 0)
return false;
now = to[now][i - 'A'];
}
return true;
}
int main()
{
int opt;
while (cin >> opt)
{
if (opt == 1) //插入
{
string str;
cin >> str;
insert(str);
}
else
{
string str;
int h;
cin >> str >> h;
cout << ((ask(h, str)) ? 'Y' : 'N') << endl;
}
}
return 0;
}
可持久化線段樹
終於到了我們的主題了。可持久化線段樹顧名思義就是可持久化的線段樹
存在的意義首先是滿足部分線段樹的要求,然后也能根據線段樹的特性解決一部分可持久化Trie的弊端。
聰明的小伙伴可以發現在一個Trie中,我們要把一條完整的子鏈完全復制下來,如果我們老版本的Trie本來就是一條鏈,這種操作無異於把Trie重新復制一遍,還是相當慢,怎么辦?
在維護一個Trie的時候,這種問題可能會讓人頭疼。但是線段樹可以完全避免,因為線段樹的樹高是完全有限的[\(Log (n)\)級別]。
基本思路還是只記錄修改過的關鍵路徑,不在關鍵路徑上的節點繼承老版本的子樹。
基本原理還是和可持久化Trie差不多,看圖和代碼基本也能理解了
代碼實現
有一點要注意的是,這個線段樹的節點關系已經是一個有向圖了,不能用滿二叉樹的性質去計算他的左右兒子。需要手動記錄。
其實就是P3919 【模板】可持久化線段樹 1(可持久化數組)的題解
#include <iostream>
using namespace std;
const int N = 5e7 + 128;
int num[N / 10];
int his[N / 10];
int h_ptr;
int lc[N], rc[N], val[N];
int p;
int n, m;
void build(int u, int l, int r) //和常規的線段樹建立差不多,就是要左右兒子不能用滿二叉樹性質算出來了,所以要手動存
{
if (l == r)
{
val[u] = num[l];
return;
}
lc[u] = ++p;
rc[u] = ++p;
int mid = (l + r) >> 1;
build(lc[u], l, mid);
build(rc[u], mid + 1, r);
}
void fork_only(int h) //僅僅只復制一個歷史版本
{
his[h_ptr++] = his[h];
}
void fork_and_edit(int h, int addr, int val_) //復制一個帶修改的版本為h的歷史版本到最新版本中(把addr這里的數修改為val)
{
int old = his[h];
int now = ++p;
his[h_ptr++] = p;
int l = 1, r = n;
while (l < r)
{
int mid = (l + r) >> 1;
if (addr <= mid) //關鍵路徑在左兒子
{
rc[now] = rc[old]; //所以右兒子直接繼承
lc[now] = ++p; //新建一個左兒子
now = lc[now];
old = lc[old];
r = mid;
}
else
{
lc[now] = lc[old]; //反之繼承左兒子
rc[now] = ++p;
now = rc[now];
old = rc[old];
l = mid + 1;
}
}
val[now] = val_;
return;
}
int query(int u, int addr)
{
int l = 1, r = n;
while (l < r)
{
int mid = (l + r) >> 1;
if (addr <= mid)
u = lc[u], r = mid;
else
u = rc[u], l = mid + 1;
}
return val[u];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> num[i];
his[h_ptr++] = ++p;
build(p, 1, n);
for (int i = 1; i <= m; i++)
{
int v, opt, loc;
cin >> v >> opt >> loc;
if (opt == 1)
{
int value;
cin >> value;
fork_and_edit(v, loc, value);
}
else
{
cout << query(his[v], loc) << endl;
fork_only(v);
}
}
return 0;
}
如果對代碼有問題歡迎評論斧正。