1. 程式人生 > >後綴自動機多圖詳解(代碼實現)

後綴自動機多圖詳解(代碼實現)

論證 point ubunt 動態添加 xtend == 擴大 min 後綴自動機

作者註:搭配理論證明類的\(SAM\)博客閱讀,效果更佳。作者水平較低,時間有限,只講實現,不再胡亂證明。

後綴自動機是一種在線的,動態添加字符擴展字符串的算法。蒟蒻深知沒圖的痛苦,這裏放一個帶詳細圖片解析的代碼實現,加深一下自己印象。順便造福後人

作圖工具:\(WPS\) \(PowerPoint\) \(For\) \(Ubuntu\)

技術分享圖片

如圖所示,添加擴展字符\(c\)後,後綴自動機中受影響的有且僅有\(p\)的後綴,所以我們只需對\(p\)的後綴的連邊情況進行更新即可。

  • 遍歷\(p\)的後綴。(\(=\)在其後綴鏈接\(/\)\(parent\) \(Tree\)上向上跳)

    • \(p\)
      的後綴中有一部分,其後面接上字符\(c\)獲得的新後綴,在添加字符\(c\)之前的原串中還未出現過。雖然原串中並沒有這樣的串,但是添加字符\(c\)後的新串中就剛剛出現了一個。這裏我們拉一條向新串\(q\)的,字符為\(c\)\(Trie\)邊。

int p = lst, q = ++node; lst = q;
len[q] = len[p] + 1;
while (!ch[p][c] && p != 0) {
    ch[p][c] = q;
    p = fa[p];//更新原串後綴的連邊
}
  • 通過這一步,我們完成了圖示中下面一部分的狀態更新。

技術分享圖片

對上面的那一部分,我們要分類討論。

  1. 字符\(c\)是第一次出現

    • 這種情況下,上面部分是不存在的。所有新生後綴都沒有在原串中的對應子串。

    • 所以所有新生後綴構成同一個等價類,只在尾部出現一次,連一條向根節點(\(1\))的後綴鏈接。

技術分享圖片

if (p == 0) {
    fa[q] = 1; //莫得其他已有後綴
}
  1. 如果有的話:

設這個點為\(x\)。可能產生的情況有:

\[len[x] = len[p]+1\]

對應下面這樣的情況:

技術分享圖片

這個時候情況比較簡單。\(p\)後綴鏈接上的所有祖先,其\(Trie\)邊也都指向這個點\(x\)。我們需要做的,就是把新產生的,原串中未出現的後綴,也就是前面圖中的下半部分接上去,完善其後綴鏈接的信息。

技術分享圖片

為什麽這麽接?因為\(x\)是等價類\(q\)字符串長度最相近的等價類嘛~那麽底下那部分的事就算完了。

int x = ch[p][c];
if (len[p] + 1 == len[x]) {
    fa[q] = x; //x是q的後綴
}

另一種情況則是這樣的:

\[len[x] >len[p]+1\]

也就是說:第一個跳到的,後面接上\(c\)產生的字符串,在原串中出現過的那個等價類(\(p\)),其接上\(c\)之後對應的那個等價類中,存在比當前這個等價類最長的字符串接上\(c\)還要長的串。顯然長出來的部分不會再和最開始圖中的下半部分吻合,如果吻合自然不被堆在底下。

也就是說,這種情況大概長這樣:

技術分享圖片

其中,藍色部分的\(end\_pos\)會擴大一個,原因是在新串在結尾處再次出現了這些串。但黃色那一部分卻並沒有作為後綴在新串結尾處再次出現。

\(end\_pos\)都不一樣,那顯然就不是同一個等價類了。結論只有一個:分家,把\(x\)分成\(x\)和新節點\(y\)兩部分。

來考慮新節點的連邊情況。向後連\(Trie\)邊等效於再在後面加字符。因為\(c\)已經是結尾處的字符了,所以再添加字符在\(c\)後面的話,結尾處自然不可能匹配得上,字符\(c\)就無法對\(end\_pos\)集合起到任何作用了。也就是說,\(x\)\(y\)\(Trie\)邊是一致的,做一次\(memcpy\)即可。

int y = ++node;
memcpy (ch[y], ch[x], sizeof (ch[x]));

我們把分出來的\(y\)設置為\(len<=len(p) + 1\)的部分,這樣\(p\)在添加字符\(c\)之後就可以正常連接到\(y\)上了。由於\(x\)\(y\)在原本長度連續的字符串集合中區間的某一長度點斷開,所以可以得到\(min\_len(x) = max\_len(y)+1\)\(y\)\(x\)在後綴鏈接上的父親。同樣的,\(y\)在後綴連接上的父親,應該把原來\(x\)在後綴連接上的父親繼承過來,可以類比於鏈表的那種插入方式。(註意處理時候的先後)

別忘了把\(q\)也拉過來一條向\(x\)的後綴鏈接,\(q\)在後綴鏈接上的父親也是對應著\(x\)呢。(參考之前的圖。)

len[y] = len[p] + 1;
fa[y] = fa[x];
fa[x] = fa[q] = y;

技術分享圖片

大概就是這樣的一個處理方式。(\(Trie\)邊實在沒法畫了\(QwQ\)

那麽\(p\)的祖先的後綴鏈接呢?節點\(p\)在後綴鏈接上的所有祖先都還接在\(x\)上呢啊\(QwQ\)

節點\(p\)在後綴鏈接上的祖先一定是\(p\)對應等價類的後綴,即長度小於\(min\_len(p)\)。也就是說,它們在添加字符\(c\)後,同樣應該連接在點\(x\)上。我們把\(p\)點的祖先的連邊做一下更新,讓它們本來連到\(x\)上的邊重定向到\(y\)上。

技術分享圖片

while (p != 0 && ch[p][c] == x) {
    ch[p][c] = y;
    p = fa[p];
}

完整代碼如下:

void Extend (int c) {
    int p = lst, q = ++node; lst = q;
    len[q] = len[p] + 1; siz[q] = 1;
    while (!ch[p][c] && p != 0) {
        ch[p][c] = q;
        p = fa[p];//更新原串後綴的連邊
    }
    if (p == 0) {
        fa[q] = 1; //q莫得其他已有後綴
    } else {
        int x = ch[p][c];
        if (len[p] + 1 == len[x]) {
            fa[q] = x; //x成為q的後綴
        } else {
            int y = ++node;
            fa[y] = fa[x];
            fa[x] = fa[q] = y;
            len[y] = len[p] + 1;
            memcpy (ch[y], ch[x], sizeof (ch[x]));
            while (p != 0 && ch[p][c] == x) {
                ch[p][c] = y;
                p = fa[p];
            }
        }
    }
}

作圖很辛苦。如果對你有幫助,請記得點一下推薦哦\(QwQ\)

後綴自動機多圖詳解(代碼實現)