淺談可持久化Trie與線段樹的原理以及實現(帶圖)


淺談可持久化Trie與線段樹的原理以及實現

引言

當我們需要保存一個數據結構不同時間的每個版本,最朴素的方法就是每個時間都創建一個獨立的數據結構,單獨儲存。

但是這種方法不僅每次復制新的數據結構需要時間空間上也受不了儲存這么多版本的數據結構。

然而有一種叫git的工具,可以維護工程代碼的各個版本,而空間上也不至於十分爆炸。怎么做到呢?

答案是版本分支,即每次創建新的版本不完全復制老的數據結構,而是在老的數據結構上加入不同版本的分支。

下面以鏈表為例

graph LR A-->B B-->C C-->D D-->E E-->F F-->G B-->Z[C_新版本] Z-->Y[D_新版本] Y-->E

新的版本是部分建立在老的版本之上的。不變的地方不變,有編號的地方就加入新版本的分支。

實現可持久化Trie

基於版本分支的思想,我們怎么建立一個可持久化Trie呢?

其次我們注意到在上面那張鏈表圖中進入下一個節點的時候,每次都要判斷有沒有我們要進入的新版本的分支,十分麻煩。有沒有方法可以保證我們向下找的節點全部是我們要的版本呢?

有方法,我們只需要記錄修改過的Trie的關鍵路徑就好了。

什么叫關鍵路徑呢?

這里先假設我們只修改Trie上的一個節點。而所謂關鍵路徑就是從Trie的根節點到修改節點的路徑,我們只創建這路徑上的節點,其余節點全部繼承一個老版本的節點。

如圖

graph TD ROOT-.c.->c ROOT-.m.->m c--a-->ca ca--t-->cat m--a-->ma ma--p-->map ROOT_NEW--c-->c; ROOT_NEW--m-->m_NEW m_NEW--a-->ma_NEW ma_NEW--p-->map ma_NEW--r-->mar_NEW mar_NEW--k-->mark_NEW

這是一個向有{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差不多,看圖和代碼基本也能理解了

graph TD A([1->4]).->B([1->2]) A.->C([3->4]) B-->D([1]) B-->E([2]) C.->F([3]) C.->G([4]) H([1->4_new])-->B H-->I([3->4_new]) I-->F I-->J([4_new])

代碼實現

有一點要注意的是,這個線段樹的節點關系已經是一個有向圖了,不能用滿二叉樹的性質去計算他的左右兒子。需要手動記錄。

其實就是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;
}

如果對代碼有問題歡迎評論斧正。


免責聲明!

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



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