這里是 SAM 感性瞎扯。
最近學了后綴自動機(Suffix_Automaton,SAM),深感其巧妙之處,故寫文以記之。
部分文字與圖片來源於 OI-Wiki,hihoCoder 與一些個人博客,鏈接在文章最底端。
在此之前請先了解有關於 SAM 的所有函數,並最好理解 OI-Wiki 上有關於 SAM 的五條引理(鏈接在最底部,引理在下文中有提到)。
一些重要的定義與引理:
- \(T\):初始狀態。
- \(\mathrm{endpos}(i)\):字符串 \(i\) 在 \(s\) 中所有出現的結束位置集合。例如當 \(s=\texttt{"abcab"}\) 時,\(\mathrm{endpos}(\texttt{"ab"})=\{2,5\}\),因為 \(s[1:2]=s[4:5]=\texttt{"ab"}\)。
- \(\mathrm{substr}(i)\):狀態(SAM 上的一個節點)\(i\) 所表示的所有子串的集合。
- \(\mathrm{shortest}(i)\):狀態 \(i\) 所表示的所有子串中,長度最短的那一個子串。
- \(\mathrm{longest}(i)\):狀態 \(i\) 所表示的所有子串中,長度最長的那一個子串。
- \(\mathrm{minlen}(i)\):狀態 \(i\) 所表示的所有子串中,長度最短的那一個子串的長度。即 \(\mathrm{minlen}(i)=|\mathrm{shortest}(i)|\)。
- \(\mathrm{len}(i)\):狀態 \(i\) 所表示的所有子串中,長度最長的那一個子串的長度。即 \(\mathrm{len}(i)=|\mathrm{longest}(i)|\)。
- \(\mathrm{link}(i)\):\(\mathrm{longest}(i)\) 最長的一個后綴 \(w\ (w\notin \mathrm{substr}(i))\) 所在的狀態。換句話說,一個后綴鏈接 \(\mathrm{link}(i)\) 連接到對應於 \(\mathrm{longest}(i)\) 的最長后綴的另一個 \(\mathrm{endpos}\) 等價類的狀態。有 \(\mathrm{minlen}(i)=\mathrm{len(link}(i))+1\)。
引理 1: 字符串 \(s\) 的兩個非空子串 \(u\) 和 \(w\)(假設 \(|u|\leq |w|\))的 \(\mathrm{endpos}\) 相同,當且僅當字符串 \(u\) 在 \(s\) 中的每次出現,都以 \(w\) 后綴的形式存在。證明詳見 OI-Wiki,下(引理 2~5)同。
引理 2:考慮兩個非空子串 \(u\) 和 \(w\)(假設 \(|u|\leq |w|\))。要么 \(\mathrm{endpos}(u)\cup \mathrm{endpos}(w)=\varnothing\),要么 \(\mathrm{endpos}(u)\subseteq\mathrm{endpos}(w)\),取決於 \(u\) 是否為 \(w\) 的一個后綴:
\[\begin{cases}\mathrm{endpos}(u)\subseteq\mathrm{endpos}(w)\quad &\mathrm{if}\ u\ \mathrm{is\ a\ suffix\ of}\ w\\\mathrm{endpos}(u)\cup \mathrm{endpos}(w)=\varnothing&\mathrm{otherwise}\end{cases} \]
引理 3:考慮一個 \(\mathrm{endpos}\) 等價類,將其中所有子串按長度非遞增的順序排序。每個子串都不會比它前一個子串長,與此同時每個子串也是它前一個子串的后綴。換句話說,對於同一等價類的任一兩子串,較短者總為為較長者的后綴,且該等價類中的子串長度恰好覆蓋整個區間 \([\mathrm{minlen,len}]\)(即排序后長度連續且不等)。
引理 4:所有后綴鏈接構成一棵根節點為 \(T\) 的樹。
- 定義后綴路徑 \(p\to q\) 表示在后綴鏈接構成的樹中 \(p\to q\) 的路徑。
引理 5:通過 \(\mathrm{endpos}\) 集合構造的樹(每個子節點的 \(subset\) 都包含在父節點的 \(subset\) 中)與通過后綴鏈接 \(\mathrm{link}\) 構造的樹相同。
(圖片來源於 OI-Wiki)
引理 6:對於一個狀態 \(t\),\(\mathrm{substr}(t)\) 中所有子串后面接上同一個字符 \(c\) 之后,新的子串仍然都屬於同一個狀態(如果該狀態存在)。
假設存在兩個字符串 \(s_p,s_{p'}\in \mathrm{substr}(t)\) 滿足 \(\mathrm{endpos}(s_p+c)\neq \mathrm{endpos}(s_{p'}+c)\)。不妨設 \(\mathrm{endpos}(s_{p'}+c)\) 包含 \(\mathrm{endpos}(s_p+c)\) 所沒有的一個位置 \(pos\),那么 \(\mathrm{endpos}(s_{p'})\) 中必定含有 \(pos-1\),而 \(\mathrm{endpos}(s_p)\) 中必定沒有。但是它們屬於同一個狀態,矛盾。得證。
一些重要的結論:(個人證明,並不非常嚴謹!)
結論 0:如果我們從任意狀態 \(t_0\) 開始順着后綴鏈接遍歷,總會到達初始狀態 \(T\)。這種情況下我們可以得到一個互不相交的區間 \([\mathrm{minlen}(t_i),\mathrm{len}(t_i)]\) 的序列,且它們的並集形成了連續的區間 \([0,\mathrm{len}(t_0)]\)。
該引理為 OI-Wiki 在 “小結” 最后一條列出的性質,根據后綴鏈接的性質和引理 3,4 易證。
結論 1:從表示 \(s[1:i]\) 的狀態 \(t_0\) 不斷跳后綴鏈接 \(\mathrm{link}\) 直到初始節點 \(T\),其遍歷到的所有狀態 \(t_0,t_1,t_2,\cdots,T\) 所包含的子串集合剛好為所有 \(s[1:i]\) 的后綴所表示的狀態,即 \(\mathrm{substr}(t_0)\cup\mathrm{substr}(t_1)\cup\mathrm{substr}(t_2)\cup\cdots\cup\mathrm{substr}(T)=\{s[x:i]\ (1\leq x\leq i)\}\)。換句話說,如果你在 \(\mathrm{SAM}_i\) 上跑所有 \(s[1:i]\) 的后綴,那么跑到的所有狀態都在后綴路徑 \(t_0\to T\) 上。
證明 1:由后綴鏈接 \(\mathrm{link}\) 的定義,對於任意一個狀態 \(t_0\),都有 \(x\) 是 \(y\) 的真后綴,其中 \(x\in\mathrm{substr(link}(t_0)),y\in\mathrm{substr}(t_0)\)。那么 \(S=\mathrm{substr}(t_0)\cup\mathrm{substr}(t_1)\cup\cdots\cup\mathrm{substr}(T)\) 中的所有子串(這里的子串是相對於 \(s[1:i]\) 而不是所包含的字符串的子串)都是 \(\mathrm{longest(t_0)}\) 即 \(s[1:i]\) 的后綴。又根據結論 0,\(S\) 中所有字符串長度覆蓋了區間 \([0,\mathrm{len}(t_0)]\),即 \([0,i]\)。得證。
結論 2:一個狀態 \(t\) 所表示的所有子串 \(\mathrm{substr}(t)\),等於初始狀態 \(T\) 到該狀態上所有路徑所形成的所有字符串。如果添加一條轉移邊 \((p,q)\),字符為 \(c\),那么相當於將 \(\mathrm{substr}(p)\) 中所有子串末尾加上字符 \(c\) 添加到 \(\mathrm{substr}(q)\) 里面。
可以結合引理 6 與結論 1 感性理解(其實是我不太會證明,但是它滿足這個性質)。如果想要直觀理解可以看下文 Case 2 中的插圖。
結論 3:考慮存在一個轉移 \((p,q)\) 且 \(\mathrm{len}(p)+1=\mathrm{len}(q)\),那么其他所有指向 \(q\) 的轉移 \((p_i,q)\) 的 \(p_i\) 都在后綴路徑 \(p\to T\) 上。
證明 3:首先,根據結論 2,我們知道如果存在轉移 \((p,q)\),字符為 \(c\),那么一定有 \(\mathrm{len}(p)+1\leq\mathrm{len}(q)\)。因此,假設有另外一個狀態 \(p'\) 存在轉移 \((p',q)\) 且 \(p'\) 不在后綴路徑 \(p\to T\) 上(顯然 \(p'\) 不能為 \(p\)),那么有 \(\mathrm{len}(p')< \mathrm{len}(p)\)。設 \(s_{p}=\mathrm{longest}(p),s_{p'}=\mathrm{longest}(p')\)。因為 \(s_p+c\) 與 \(s_{p'}+c\) 同屬於 \(\mathrm{substr}(q)\),且 \(|s_{p'}+c|<|s_p+c|\),所以根據引理 3,\(s_{p'}+c\) 是 \(s_p+c\) 的后綴。所以 \(s_{p'}\) 是 \(s_p\) 的后綴。因此,根據引理 2,\(\mathrm{endpos}(s_p)\subsetneq \mathrm{endpos}(s_{p'})\ (p\neq p')\)。因此,根據引理 5,\(p'\) 在 \(\mathrm{link}\) 樹上一定為 \(p\) 的父節點。這與假設矛盾。得證。
結合上圖以更好理解(圖片來源於 hihoCoder)。
推論 3:指向狀態 \(q\) 的轉移 \((p_i,q)\) 的所有狀態 \(p_i\),在 \(\mathrm{link}\) 樹上一定為一條深度遞減的鏈 \(p_0\to p_x\),且有 \(\mathrm{minlen}(p_x)+1=\mathrm{minlen}(q),\mathrm{len}(p_0)+1=\mathrm{len}(q)\)。
SAM 的構造方式:
類似數學歸納法:假設已經構造好了 \(\mathrm{SAM}_{i-1}\),其狀態為 \(las\),這樣所需要做的就是添加一個字符 \(s_i\)。
首先,\(\mathrm{SAM}_{i-1}\) 是無法表示 \(s[1:i]\) 的(因為它只接受 \(s[1:i-1]\) 的子串),所以我們新建一個節點 \(cur\) 表示至少包含 \(s[1:i]\) 的狀態,顯然有 \(\mathrm{len}(cur)=i\)(或者說等於 \(\mathrm{len}(las)+1\))。可以發現 \(\mathrm{endpos}(s[1:i])=\{i\}\),因為 \(s[1:i]\in\mathrm{substr}(cur)\),而 \(s[1:i]\) 在 \(s[1:i]\) 中顯然只以 \(i\) 為結束位置出現。
- 為什么說至少包含?因為 \(s[1:i]\) 的其它后綴也可能只以 \(i\) 為結束位置出現。例如 \(s=\texttt{aab}\)(假設 \(i=3\)),那么不僅是 \(\mathrm{endpos}(\texttt{"aab"})=\{3\}\),\(\texttt{"ab"}\) 與 \(\texttt{"b"}\) 的 \(\mathrm{endpos}\) 集合也為 \(\{3\}\)。
Case 1:根據引理 1 與 2,如果不考慮重復狀態,那我們只需要將后綴路徑 \(las\to T\) 上的所有狀態往 \(cur\) 連一條字符為 \(s_i\) 的轉移邊,並將 \(\mathrm{link}(cur)\) 設為 \(T\) 即可(因為后綴路徑 \(las\to T\) 上的所有點剛好表示了 \(s[1:i-1]\) 的所有后綴,在其后面添加字符 \(s_i\) 就可以表示 \(s[1:i]\) 的所有后綴)。這其實是 Case 1,即后綴路徑 \(las\to t\) 上所有狀態都沒有字符 \(s_i\) 的轉移邊。容易發現這種情況僅在 \(s_i\) 未在 \(s[1:i-1]\) 中出現過時發生。
如果有重復怎么辦?我們從 \(las\) 開始跳到的不重復的所有狀態往 \(cur\) 連一條 \(s_i\) 的轉移邊,並設遇到的第一個重復的轉移為 \((p,q)\)。為什么不能再添加 \((p,cur)\) 的 \(s_i\) 的轉移了?因為這樣會導致從 \(T\to q\) 和 \(T\to cur\) 可以表示相同的子串,破壞了 SAM 的性質。記在后綴路徑 \(las\to T\) 跳到 \(p\) 的上一個狀態為 \(p'\)(即 \(\mathrm{link}(p')=p\))。顯然 \((p',cur)\) 有一條字符為 \(s_i\) 的轉移。
此時 \(cur\) 的后綴鏈接應該怎么連?我們想要滿足 \(\mathrm{len(link(}cur))+1=\mathrm{minlen}(cur)\)。這個 \(\mathrm{minlen}(cur)\) 實際上就是 \(\mathrm{minlen}(p')+1\),那么 \(\mathrm{len}(p)=\mathrm{minlen}(p')-1=\mathrm{len(link(}cur))-1\)(因為 \(\mathrm{link}(p')=p\))。
Case 2:\(\mathrm{len}(q)=\mathrm{len}(p)+1\),此時 \(q\) 中包含的最長子串就是 \(p\) 中的最長子串接上字符 \(s_i\)。那么只需將 \(\mathrm{link}(cur)\gets q\) 即可。因為 \(\mathrm{len}(q)=\mathrm{len}(p)+1=\mathrm{len(link(}cur))\),剛好是我們想要的。
(圖片來源於 hihocoder)
上圖中,在添加 \(s_5=\texttt{a}\) 時,從 \(T(S)\to 1\) 已經有了現成的字符 \(s_5\) 的轉移。此時 \(p=T,q=1\)。因為 \(\mathrm{len}(1)=\mathrm{len}(T)+1\ (1=0+1)\),所以直接將 \(\mathrm{link}(6)\gets 1\) 即可。
注意上圖中狀態 \(4,5,6\) 所表示的子串,可以發現狀態 \(6\) 所表示的子串是狀態 \(4,5\) 所表示的所有子串后接上字符 \(s_5\) 得到的。這很好地驗證了結論 2,也非常直觀形象地闡釋了 Case 2 的情況。
Case 3:\(\mathrm{len}(q)>\mathrm{len}(p)+1\),此時 \(q\) 對應了 \(s[1:i-1]\) 的更長的子串(感覺 OI-Wiki 這里講得不是很明白)。此時除了把 \(q\) 拆開來別無他法。具體來說,將 \(q\) 所表示的所有長度不大於 \(\mathrm{len}(p)+1\) 的子串提出來,丟給一個新建的狀態 \(q'\),然后將 \(\mathrm{link(cur)}\gets q'\),同時添加轉移 \((q',cur)\),也就是說我們憑空創造了一個滿足 Case 2 的狀態 \(q\)。
顯然,無中生有是要付出一些時間代價的。先考慮 \(q\) 和 \(q'\) 的內部情況。具體的,\(\mathrm{link}(q)\) 應改為 \(q'\),而 \(\mathrm{link}(q')\) 應該繼承原來的 \(\mathrm{link}(q)\)。此外,一些本來轉移到 \(q\) 的轉移 \((p_i,q)\) 也應該變為 \((p_i,q')\)。具體地,我們從后綴路徑 \(p\to T\) 繼續往上跳,沿路徑跳到的所有狀態 \(p_i\),如果有一條 \((p_i,q)\) 的轉移,那么將其改為 \((p_i,q')\)。
(圖片來源於 hihocoder)
特別的,如果跳到一個狀態 \(P\),它沒有 \((P,q)\) 的轉移,此時退出即可,因為根據結論 3,此時再往上跳也不可能出現另外一個狀態 \(P'\) 有 \((P',q)\) 的轉移。換句話說,我們找到后綴路徑 \(p\to T\) 上從 \(p\) 開始的一段有 \((p_i,q)\) 轉移的所有狀態,並將其修改為 \((p_i,q')\)。因為根據推論 3,路徑上有且僅有一段狀態 \(p_i\) 有 \((p_i,q)\) 的轉移,且所有 \(p_i\) 所表示的子串加上字符 \(s_i\),剛好能表示狀態 \(q\) 原本所表示的所有長度不大於 \(\mathrm{len(p)}+1\) 的子串,如上圖。
(圖片來源於 hihocoder)
上圖中,我們把 \(q=3\) 的不大於 \(\mathrm{len}(T)+1=1\) 的所有子串提出來,丟給一個新建的狀態 \(q'=5\),然后 \(\mathrm{link}(4\ (cur))\gets 5\ (q')\) 並添加狀態 \((5,4)\)(即 \((q',cur)\))。內部,\(\mathrm{link}(3\ (q))\gets 5\ (q')\),同時 \(\mathrm{link}(5\ (q')) \gets T\)(原來的 \(\mathrm{link}(3)\))。最后從 \(T\ (p)\) 往上跳后綴連接直到不存在連向 \(3\) 的路徑或到了初始狀態 \(T\)(當然,這里的例子只有 \(T\) 一個點,不過我們需要知道並不一定會跳到 \(T\),因為有可能跳到中間的某個狀態 \(P\) 時就沒有轉移 \((P,3\ (q))\) 了),並將所有連向 \(3\ (q)\) 的轉移連向 \(5\ (q')\),即 \((T,3)\) 變為了 \((T,5)\)。
綜合上述三種情況,我們就可以在線性時間內建造出一個字符串 \(s\) 的 SAM(關於其時間復雜度為線性的證明,詳見 OI-Wiki)。
部分額外信息在代碼與應用中有提到。
代碼與應用:
請移步 SAM 做題筆記。
一些資料:
如發現錯誤或有不理解的地方可以在下方評論區留言,我會盡快修改。