1. 程式人生 > >字尾自動機(SAM)速成手冊!

字尾自動機(SAM)速成手冊!

正好寫這個部落格和我的某個別的需求重合了。。。我就來講一講SAM啦qwq

字尾自動機,也就是SAM,是一種極其有用的處理字串的資料結構,可以用於處理幾乎任何有關於子串的問題,但以學起來異常困難著稱(在機房裡,最先學會SAM的永遠是大佬(比如litble和zyf(他在退役前就學了)))。

但是!!!當你學了SAM並熟練地刷了幾道題後,你會發現——你之前為了學SAM而強行理解的許多定理,對你應用SAM一點用處也沒有!為了引出構造演算法,幾乎所有部落格都會詳細地解釋“你為啥要這樣做”,然鵝。。。

SAM完全可以當成黑盒來用!!!!!!

所以我打算寫一篇SAM速成部落格。。。保證即學即會!

在構建之前你不得不知道的

Warning:想徹底理解字尾自動機嗎?那你有好訊息了!請立即關閉此頁面,在百度裡搜尋“字尾自動機 陳立傑”,開始愉快的學習吧!(講真,陳老師的ppt是講的最好的,別的部落格無能出其右者)

SAM是一個DAG(有向無環圖),每個點代表一個"狀態",邊代表狀態轉移,邊上有一個字母。SAM有一個起始狀態(稱為起點),從起點開始,沿著邊不斷走下去,就可以得到一個字串。記當前停留節點為\(x\),走出來的字串為\(S\),稱節點\(x\)可代表字串\(S\)。記\(x\)可代表的串中長度最長的串的長度為\(len(x)\)

另外,除起點外的每個節點還擁有一個“字尾連結“

,記作\(fa(x)\)。字尾連結組成了一棵樹,別的性質在構建完之後再講。

儲存SAM利用的是類似於Trie樹的儲存結構,即使用\(ch[][26]\)陣列儲存狀態轉移的邊。

知道了這些,構建SAM的工作就可以開始了。

開始建造字尾自動機

準備工作:建立陣列\(ch,fa,len\),準備指標\(last,cnt\)。SAM的構造方法是不斷地向已經建好的SAM中加入新的節點。\(last\)表示上一個被插入的節點,\(cnt\)表示SAM中的節點數量。一開始,\(last=cnt=1\),表示只有一個起點的初始SAM。

接下來,假設要往SAM里加入一個字元\(x\)

  1. 新建節點\(np=++cnt\)
    。新建節點\(p\)\(p=last\)。$ last=np$。
  2. 如果不存在\(ch[p][x]\),令\(ch[p][x]=np,p=fa[p]\)。重複此步驟。
  3. 如果到最後還沒有一個\(p\)擁有兒子\(x\),令\(fa[np]=1\)。退出過程。
  4. \(ch[p][x]​\)出現時,令\(q=ch[p][x]​\)。如果\(len[q]==len[p]+1​\),令\(fa[np]=q​\)。退出過程。
  5. 否則有點麻煩。新建節點\(nq=++cnt\),將\(q\)的兒子都複製給\(nq\),令\(len[nq]=len[p]+1\)
  6. \(fa[nq]=fa[q],fa[q]=fa[np]=nq\)
  7. \(p\)開始沿著字尾連結,將所有\(ch[p][x]==q\)的節點的\(ch[p][x]\)都替換成\(nq\)

將你的字串的所有字元都一一進行如上操作後,你就得到了用你的字串構建出來的SAM。

你不需要知道為什麼這麼操作可以得到SAM,你只需要記下以下的程式碼,做幾道題強化記憶,然後就可以用SAM的性質來秒題了。

void insert(int x)
{
    int np=++cnt,p=last;
    len[np]=len[p]+1,last=np;
    while(p&&!ch[p][x])ch[p][x]=np,p=fa[p];
    if(!p)fa[np]=1;
    else
    {
        int q=ch[p][x];
        if(len[q]==len[p]+1)fa[np]=q;
        else
        {
            int nq=++cnt;len[nq]=len[p]+1;
            memmove(ch[nq],ch[q],sizeof(ch[nq]));
            fa[nq]=fa[q],fa[np]=fa[q]=nq;
            while(ch[p][x]==q)ch[p][x]=nq,p=fa[p];
        }
    }
}

字尾自動機的奇妙性質

現在,你已經擁有SAM了,你需要知道它有什麼用。這裡列舉了SAM的一些基本且常用的性質。

請牢記以下每一條內容!都十分有用!不要去問“為什麼是這樣的”!(如果一定要問,請參照上文藍色放大的Warning)

首先,SAM的點數與邊數都是\(O(n)\)的。記住,由於每次插入最多新建兩個點,所以應該開字元總量兩倍的空間。計算空間時別忘了你開了26倍的\(ch\)陣列。

在SAM上從起點開始沿著邊隨便走走,得到的一定是子串。同時,每一個子串都可以在SAM上走出一條唯一對應的路徑。也就是說,子串和SAM上從起點開始的每一條路徑一一對應。路徑數等於子串數。

起點可以看做是代表空串的點。

重點:定義子串的\(right\)集合:這個子串在原串中所有出現的位置的右端點的集合。

比如說:AAAABBAAAAABAAABBAA

子串AAB出現了3次,右端點集合為\(\{5,12,16\}\)。這就是子串AAB\(right\)集合。

一個節點能夠代表的所有子串的\(right\)集合是一樣的。\(right\)集合相等的子串一定被同一個節點代表。(所以,我們會使用“節點的\(right\)集合”這個說法。)兩個節點的\(right\)集合之間要麼真包含,要麼沒有交集。若節點\(y\)\(right\)集合包含了節點\(x\)\(right\)集合,那麼\(y\)能代表的子串均為\(x\)能代表的子串的真字尾。

重點:定義節點\(x\)的字尾連結\(fa(x)\):如果有一些節點的\(right\)集合包含了\(x\)\(right\)集合,\(fa(x)\)是其中\(right\)集合的大小最小的那一個。

字尾連結們組成了一棵“字尾連結樹”(不是字尾樹)。字尾連結樹的根為起點。若節點\(y\)\(right\)集合包含了節點\(x\)\(right\)集合,那麼\(y\)在後綴連結樹上是\(x\)的祖先。

一個節點的\(right\)集合等於他在後綴連結樹上的所有兒子的\(right\)集合的並集。而且兒子的\(right\)集合之間兩兩沒有交集。

每個節點能代表的子串的長度範圍是一段連續的區間。這很好理解,因為它們的結束位置都是相同的。

我們求出每個節點能代表的最長串的長度(即\(len(x)\))了,那最短長度呢?其實就等於字尾父親節點的\(len+1\)。也就是說,所有本質不同的子串的數量等於\(\sum len(x)-len(fa(x))\)

總結

以上就是SAM的基本性質~對於一道特定的題,你可能需要通過上面的性質推出你需要的新性質。如果你還有什麼疑問可以向我留言,我(在退役前)會在一天之內回覆的!(你也可以去問更強的boshi和litble,別去問zyf因為他已經退役了。)

題單我就不給了,因為網上有很多很多。。。

當然,如果你立志要當大佬。。。那趕緊開啟陳立傑的ppt吧=。=

感謝您的觀看qwq!