算法 {持久化Trie树}

算法 {持久化Trie树}

持久化Trie

定義

#背景#: 有2個操作{插入(字符串),查詢(字符串)} (這是一個標準TRIE樹的應用場景), 但是需要同時維護(版本信息) 就像一個Git系統一樣, 可以回退版本 也可以在某個版本的基礎上再進行插入操作;
. 顯然要實現一個(節點為一個TRIE樹)的Git系統, 你用暴力的方式 (每個節點 就是一個單獨的TRIE) 但這會超時, 而因為 TRIE樹的插入操作(插入一個長度為N的字符串) 他只會使用到(N+1)個節點, 即當前TRIE為A 往裡面插入一個字符串後 得到TRIE為B 那麼 B只有N+1個節點 是與A不同的, 而B的其他所有節點的信息 和A的 是完全一樣的, 因此借用指針/引用的概念 我們可以讓B裡面的這些(與A相同的)節點 就指向A的節點 (兩者完全相同), 因此 AB這兩個TRIE 有如下性質: (本質結構相同 這點非常重要 不管動態開點這個性質 他倆都是(每個點有Branches個分支)) 基於這個性質 比如說對於某個字符串C 他對應在A裡的節點為a 對應在B裡的節點為b, 那麼此時有2種情況: 要麼a=b(這說明你的插入操作 不會訪問到這個節點 換句話說C不是你插入的字符串的前綴, 此時a/b的值 可能是nullptr即沒有開闢節點 也可能!=nullptr 雖然開闢了節點 但兩者的節點是同一個內存), 要麼a!=b 這說明 你B裡新插入的字符串 會訪問到這個節點(即C是你插入的字符串的前綴) 那麼此時a,b兩者的內存一定不同(雖然他倆都代表C這個字符串 這就是前面講的本質結構是相同的);

#算法#: 每個節點 一定有一個int Son[ Branches]; 他記錄 (當前節點的兒子的下標) 就相當於是指針(指向了他的兒子 -1表示兒子還沒有開闢), 這種存儲方式的好處在於 你只要得到根節點root 就等價於 你得到了整個TRIE樹 (順藤摸瓜); 比如 此時的TRIE為A 他的根節點為RA 其版本號為VA, 我要在他的基礎上 進行插入操作(插入字符串012) 然後得到一個新版本VB, 首先 申請一個新節點RB(因為你插入的字符串 一定會訪問根節點 只要是會訪問到的節點 就要原版本的節點 不同) 執行Node[RB] = Node[RA] (由於一個根節點 就代表了一整個TRIE樹, 因此你這個操作 意味著 此時RB就代表了A這個TRIE!), 因為要插入012這個字符串 然後你要移動到son = Node[ RB].Son[ 0] 此時有2種情況: {1(son!=-1):[這說明 你要訪問的這個節點 在A裡是存在的(此時不同共用 你只要會訪問的節點 就必須和A的不同), 你需要開闢一個新節點new 然後讓Node[ new] = Node[son] 這個操作非常重要 這意味著(以new為根的樹 此時就是以son為根的樹) 最後讓Node[ RB].Son[0] = new 更改當前節點的這個兒子], 2(son=-1):[此時就開闢一個新節點new 然後讓Node[ RB].Son[0] = new即可 因為son在A裡而沒有子樹的 因此不需要向1:一樣 做拷貝子樹的操作}; 然後繼續遍歷整個字符串 操作都是一樣的;

相關定義

#區間版本的查詢#
一般的查詢 即回退到某個版本R, 然後在R這個TRIE上進行查詢 這很簡單;
假設TRIE的修改操作只有插入, 假如說兩個版本L,R 且(L是R的祖先版本 即在L的基礎上 新添加了若干個字符串S後得到了R版本), 你需要求 新添加的這些字符串S的信息, 有點類似於前綴和思想, 我們舉個實際的例子:
字符串序列S[N], 對S[l...r]這個區間的字符串 進行查詢(可以認為是 把這些S[l...r]字符串 放到一個TRIE裡 進行查詢操作), 做法是: 令第i版本的TRIE為S[...i] 即此時你有N個TRIE版本, 然後每個節點 存儲count 表示(以當前字符串為前綴的元素個數, 即你插入一個字符串"abc"時 會執行"a".count++, "ab".count++, "abc".count++), 那麼此時 假如你要查詢一個字符串"xyz"S[l+1, ..., r]這些字符串裡 進行查詢, 令"x"在L版本的TRIE裡的節點為LID 在R版本的TRIE裡為RID (這裡一定要聯繫的TRIE的(不同版本的TRIE樹的本質結構是相同的)這一性質) @IF(LID是不存在的):[你只比較RID即可 即如果RID.count>0 說明"x"S[l+1 ...r]裡的某個字符串的前綴 那麼可以繼續往下遍歷] @ELSE(LID存在):[令c = RID.count - LID.count 這表示的 S[l+1 ...r]裡面 有多少個字符串 他的前綴是等於"x"的, 如果c>0 說明滿足條件 則可以繼續往下遍歷]; 這個count屬性 本身也是有前綴和的性質的, 即對於"abcd" 你會令"a".count += 1, 也就是 比如"a".count = 2 那麼說明有2個字符串 他的前綴是"a", 再根據 此時只有插入操作(沒有修改/刪除) 因此對於任何字符串 他在R版本的count值 一定是>= 其在L版本的count值的, 因此 這兩個值 是可以進行相減的 (這裡的邏輯要搞清楚) 你讓"xxx"在R版本的count - "xxx"在L版本的count 這個值 就等於: S[l+1, ...,r]這些字符串裡 有多少個 他的前綴是等於"xxx";

算法

以插入的每一個元素 作為新版本

性質

一個序列A, 第i個版本的TRIE樹 存儲A[0...i]這些數, 對於區間查詢A[l...r] 就對應為 r版本的TRIE樹RT 減去 第l-1版本的TRIE樹LT, 這裡的減去 是指的RT與LT一一對應的節點信息的相減, 即我們構造一個TRIE樹T 他的節點x的信息 等於RT的x節點信息減去LT的x節點信息, 那麼會有T等於存儲A[l...r]這些數的TRIE樹; 當然你要保證 這個節點信息 是符合相減性質的;

@DELI;

代碼
template< class _TypeItem_, int _BranchesCount_>
class ___TrieTree_Persistent{
//<
// 一個TRIE樹(即某個版本的TRIE樹) 他對應與一個(字符串的集合), 換句話說 一個TRIE樹 和 一個字符串集合 是等價的;
// `_TypeItem_`: Trie樹所存儲的元素的數據類型, 比如`TypeItem=string` 那麼他的每個*字符*就對應Trie上的*節點*, 一個字符串的最大長度 決定了Trie樹的高度;
// `_BranchesCount_`: 每個點的分支個數;
public:
class __Node_{
public:
    int SonId[ _BranchesCount_]; // `b=SonId[a]`: @IF(`b==-1`):[當前節點的第`a`個兒子暫未開闢], @ELES:[兒子為`Nodes[b]`];
    int Count; // 對於某個TRIE樹(他對應的字符串集合為`S`), 對於任意字符串C 令其對應在TRIE的節點為`x` 則`x.Count`表示:[`S`中 前綴為`C`的字符串的個數];
    //< 比如Trie裡只有一個"abc", 那麼`["a"/"ab"/"abc"].Count==1`;
    __Node_(){ Initialize();}
    void Initialize(){
        memset( SonId, -1, sizeof( SonId));  Count = 0;
    }
    friend ostream& operator<<( ostream & _cout, const __Node_ & _a){
        __Debug_list( _a.SonId, _a.Count, "\n");
        return _cout;
    }
};

vector<__Node_> Nodes; // `Nodes[a].SonId[b]=c`: `a`號節點的 第`b`個分支兒子的編號為`c` (如果`c=-1` 說明這個兒子節點還尚未存在);
//< 動態開點 當前已經使用了`[0,...,Nodes.size()-1]`這些節點;
vector<int> VersionRoot; // `VersionRoot[a]=b`: `a`號版本的Trie樹的根節點為`Nodes[b]`;
//< 當前已經有`[0,...,VersionRoot.size()-1]`這些版本;
vector<int> VersionFa; // `VersionFa[a]=b`: `a`號版本的父節點為`b`, 如果`b==-1` 說明`a`版本在Git樹中為*根節點*;
//< `VersionFa.size()=VerionRoot.size()`

void Initialize( int _nodesEstimateMaximum_, int _versionsEstimateMaximum){
//< `nodesEstimateMaximum`:[預估的總節點個數];  `versionsEstimateMaximum:[預估的總版本個數];
    Nodes.reserve( _nodesEstimateMaximum_);  Nodes.clear();
    VersionRoot.reserve( _versionsEstimateMaximum);  VersionRoot.clear();
    VersionFa.reserve( _versionsEstimateMaximum);  VersionFa.clear();
}
void Insert( pair<int,int> _flag, const _TypeItem_ & _str){
//< @IF(`flag.first=1`):[以`flag.second`號版本為父節點 創建一個新的版本(版本號為`VersionRoot.size()`)];
//  @ELIF(`flag.first==-1`):[直接修改`flag.second`號版本 但要確保*這個版本一定是Git上的葉子節點* 即沒有`VersionFa[?]=flag.second`];
//  @ELIF(`flag.first==0`):[直接創建一個*沒有父節點*的新版本(版本號為`VersionRoot.size()`)];
    int curId; // 當前所操作Trie樹的根節點;
    if( _flag.first == 1){ // 在父版本的基礎上 創建新版本
        ASSERT_( IsInInterval_( _flag.second, 0, (int)VersionRoot.size()-1, true));
        curId = Nodes.size();
        Nodes.push_back( Nodes[ VersionRoot[ _flag.second]]); // 複製
        VersionRoot.push_back( curId);
        VersionFa.push_back( _flag.second);
    }
    else if( _flag.first == 0){ // 創建(無父節點的)新版本
        curId = Nodes.size();
        Nodes.emplace_back(); // 完全*空的*樹;
        VersionRoot.push_back( curId);
        VersionFa.push_back( -1);
    }
    else if( _flag.first == -1){ // 在已有版本上進行操作
        ASSERT_( IsInInterval_( _flag.second, 0, (int)VersionRoot.size()-1, true));
        ASSERT_MSG_( "確保不存在`VersionFa[?]==flag.second`, 即該版本是Git上的*葉子節點*;");
        curId = VersionRoot[ _flag.second];
    }

    for( auto cur : _str){
        int branchId = @TODO(`cur`對應的分支ID);
        ASSERT_WEAK_( ::Tools::IsInInterval_( branchId, 0, _BranchesCount_-1, true));

        if( Nodes[ curId].SonId[ branchId] == -1){ // 當前要訪問的節點不存在
            Nodes[ curId].SonId[ branchId] = Nodes.size();  Nodes.emplace_back();
        }
        else{ // 當前要訪問的節點已經存在
            if( _flag.first == 1){ // 父節點已經有當前這個節點了, 需要新拷貝一個節點
                Nodes.push_back( Nodes[ Nodes[ curId].SonId[ branchId]]);
                Nodes[ curId].SonId[ branchId] = (int)Nodes.size() - 1;
            }
        }

        curId = Nodes[ curId].SonId[ branchId];
        //>< `_str`的`[...cur]`這個前綴 對應在Trie樹中的節點編號為`curId`;
        Nodes[ curId].Count ++;
    }
    //>< `_str` 對應在Trie樹中的節點編號為`curId`;
}
int Query( int _lv, int _rv, const _TypeItem_ & _str){
//< 令`T`為一個`TypeItem`的集合, 則當前函數是在`T`這個集合裡 對`_str`進行查詢, `T`的定義如下:
//  @IF(`lv=-1`):[`rv`這個版本的Trie裡所有的`TypeItem`組成的集合 即為`T`];
//  @ELSE(`lv!=-1`):[確保`lv`在Git樹中一定是`rv`的*祖節點*, 令`lv`版本的TRIE樹所對應的字符串集合為`SL` 令`rv`版本對應的字符串集合為`SR`, 則`T`等於差集`SR/SL`];
//  . 注意 這裡的`lv,rv` 和前綴和思想很像 但有一點不同, 這裡是不包含`lv`的 即實際上你查詢的是`[lv+1, ..., rv]`這個區間 (即這些`(lv,rv]`版本所調用的修改操作);
    ASSERT_( _lv>=-1 && _lv<=_rv && _rv>=0 && _rv<(int)VersionRoot.size());
    int rightId = VersionRoot[ _rv];
    int leftId;
    if( _lv == -1){
        leftId = -1;
    }
    else{
        ASSERT_MSG_( "確保`lv`在Git樹中一定是`rv`的*祖節點*");
        leftId = VersionRoot[ _lv];
    }
    
    for( auto cur : _str){
        int branchId = @TODO(`cur`對應的分支ID);
        ASSERT_WEAK_( ::Tools::IsInInterval_( branchId, 0, _BranchesCount_-1, true));

        int count; // `T`裡面 前綴為`str[...cur]`的字符串的個數
        if( Nodes[ rightId].SonId[ branchId] == -1){
            count = 0;
        }
        else{
            count = Nodes[ Nodes[rightId].SonId[branchId]].Count;
            if( leftId != -1  &&  Nodes[ leftId].SonId[ branchId] != -1){
                count -= Nodes[ Nodes[leftId].SonId[branchId]].Count;
            }
        }

        if( count > 0){
            
        }
        else{
            //>< `T`集合是空的;
            return;
        }

        ASSERT_( Nodes[ rightId].SonId[ branchId] != -1);
        if( leftId != -1){ leftId = Nodes[ leftId].SonId[ branchId];}
        rightId = Nodes[ rightId].SonId[ branchId];
        //>< `_str`的`[...cur]`這個前綴 對應在Trie樹中的節點編號為`curId`;
    }
    //>< `_str` 對應在Trie樹中的節點編號為`curId`;
}
friend ostream& operator<<( ostream & _cout, const ___TrieTree_Persistent & _tr){
    __Debug_list( "\n", "*Trie-Debug-Begin*", "\n");
    __Debug_list( _tr.VersionRoot, _tr.VersionFa, "\n");
    FOR_( v, 0, (int)_tr.VersionRoot.size()-1){
        function<void(int,string)> dfs = [&]( int _curId, string _str){
            bool isLeaf = true;
            for( int branchId = 0; branchId < _BranchesCount_; ++branchId){
                if( _tr.Nodes[ _curId].SonId[ branchId] != -1){
                    isLeaf = false;
                    dfs( _tr.Nodes[ _curId].SonId[ branchId], _str + char('0' + branchId));
                }
            }
            if( isLeaf){
                    __Debug_list( _str, "\n");
            }
        };
        __Debug_list( "Version(" + to_string(v) + ")-Begin", "\n");
        dfs( _tr.VersionRoot[ v], "");
        __Debug_list( "Version(" + to_string(v) + ")-End", "\n");
    }
    __Debug_list( "*Trie-Debug-End*", "\n");
    return _cout;
}
}; // class ___TrieTree_Persistent


___TrieTree_Persistent< int, 2> Tr;
Tr.Initialize( N*30, M);
FOR_( i, 0, N-1){
    if( i == 0){ Tr.Insert( {0,1}, ?);}
    else{ Tr.Insert( {1,i-1}, ?);}
}

性質

之前講過 對於只有(插入)操作的TRIE, 要判斷一個字符串/前綴 是否存在, 你可以直接通過Son[]兒子的指針 來判斷其是否開闢 也就意味著 他是否存在;
但是在(區間版本的查詢)這裡, 你要涉及到2個版本之間的相減 因此就需要用到count這個額外信息來維護 (表示某個前綴出現的次數), 對於某字符串S 用Version[R].count - Version[L].count 這個值 就代表了 從(L版本之後開始 到R版本結束) 這個階段 所新加的字符串集合裡面 前綴為S的字符串的個數;

例題

@LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=133611997;

  • 25
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值