<更新提示>
本文的圖片材料多數來自\(\mathrm{hihocoder}\)中詳盡的\(SAM\)介紹,文字總結爲原創內容。ios
<第一次更新>
<正文>
首先咱們要定義肯定性有限狀態自動機\(\mathrm{DFA}\),一個有限狀態自動機能夠用一個五元組\((\mathrm{S},\Sigma,\mathrm{st},\mathrm{end},\delta)\)表示,他們的含義以下:c++
\(1.\) \(\mathrm{S}\) 表明自動機的狀態集
\(2.\) \(\Sigma\) 表明字符集,也稱字母表
\(3.\) \(\mathrm{st}\) 表明自動機的起始狀態
\(4.\) \(\mathrm{end}\) 表明自動機的終止狀態,也稱接受狀態
\(5.\) \(\delta\) 表明自動機的轉移函數算法
例如,\(\mathrm{Trie}\)樹就能夠當一個自動機,\(\mathrm{Trie}\)樹的點集就是自動機的狀態集,小寫字母集就是字符集,\(\mathrm{Trie}\)的根就是自動機的起始狀態,\(\mathrm{Trie}\)的每個葉節點就是自動機的終止狀態,每個點的出邊集合就表明自動機的轉移函數。數組
理解的有限狀態自動機的定義了之後,咱們就能夠說\(\mathrm{AC}\)自動機,後綴自動機都是肯定性有限狀態自動機。網絡
若是一個字符串\(s\)由某一個自動機\(T\)的起始狀態\(\mathrm{st}\)開始,根據字符串走轉移邊能夠到達\(T\)的一個終止狀態\(\mathrm{ed}\),那麼咱們就稱自動機\(T\)能夠識別串\(s\),記爲\(T(s)=\mathrm{true}\)。數據結構
後綴自動機\(\mathrm{SAM}\)是一個最簡有限狀態自動機,識別且僅識別某個字符串\(s\)的全部後綴。函數
其中,最簡的含義爲狀態數最少,也就是節點數最少。學習
例如,字符串\(s=aabbabd\)的後綴自動機如圖所示:優化
它的起始狀態\(\mathrm{st}=S\),終止狀態\(\mathrm{ed}=9\),從\(S\)到\(9\)全部藍色轉移邊構成的路徑就表明字符串\(s\)的每個後綴\(\{aabbabd,abbabd,bbabd,babd,abd,bd,d\}\)。ui
注意,圖中的綠色虛線不是自動機的轉移邊,是後綴自動機特有的後綴連接\(\mathrm{Suffix-link}\)。
接下來,咱們將根據有限狀態自動機的五個元素來介紹後綴自動機。
首先,咱們稱字符串\(s\)的一個子串在原串的全部的出現位置的右端點集合爲\(\mathrm{endpos}\)集合。例如\(s=aabbabd\),那麼\(\mathrm{endpos}(b)=\{3,4,6\},\mathrm{endpos}(ab)=\{3,6\}\)。
對於\(\mathrm{endpos}\)集合相等的一些子串,咱們稱其爲一個\(\mathrm{endpos}\)等價類,例如\(\mathrm{endpos}(aabb)=\{4\},\mathrm{endpos}(abb)=\{4\}\),它們屬於同一個\(\mathrm{endpos}\)等價類。
那麼咱們就能夠定義後綴自動機的狀態集\(\mathrm{S}\)爲字符串\(s\)當中全部的\(\mathrm{endpos}\)等價類。 換句話說,咱們求出\(s\)每個子串的\(\mathrm{endpos}\)集合,把相同的歸爲一類,那麼剩下的這些集合每個分別就表明了後綴自動機上的一個狀態。
因此一個\(\mathrm{endpos}\)等價類就表明了若干個字符串,咱們能夠用\(\mathrm{substr}(p)\)表明狀態\(p\)的全部字符串,\(\mathrm{long}(p)\)表明最長的那一個字符串,\(\mathrm{short}(p)\)表明最短的那一個字符串。例如\(\mathrm{substr}(4)=\{bb,abb,aabb\}\ ,\ \mathrm{long}(p)=aabb\ ,\ \mathrm{short}(p)=bb\)。
在後綴自動機當中字符集的定義和有限狀態自動機中字符集的定義是同樣的,一般有小寫英文字母集,大寫英文字母集,阿拉伯數字集,正整數集等等,依據具體狀況肯定。
咱們定義後綴自動機有惟一的起始狀態\(S\)和終止狀態\(P\),全部節點均由\(S\)出發可達,全部節點都可以到達\(P\)。
對於一個狀態\(p\),咱們記\(\mathrm{substr(p)}\)中全部字符串下一個位置可能遇到的字符集合爲\(\mathrm{next}(p)\),例如\(\mathrm{next}(S)=\{a,b,d\}\ , \ \mathrm{next}(8)=\{b,d\}\)。
咱們不難發現後綴自動機具備一個性質,就是對於一個狀態\(p\)的可能後接字符\(c\),全部\(\mathrm{substr}(p)\)內的字符串加上\(c\)後的字符串都屬於同一個狀態。就好比狀態\(4\)有\(\mathrm{substr}(4)=\{bb,abb,aabb\}\ ,\ \mathrm{next}(4)=\{a\}\),他們接上\(a\)後獲得\(\{bba,abba,aabba\}\),這些字符串都屬於\(\mathrm{substr}(6)\)。
這樣咱們就能夠定義後綴自動機的轉移函數\(\mathrm{trans}\)了:對於狀態\(p\)和字符\(c\in\mathrm{next}(p)\),\(\mathrm{trans}(p,c)=x\ ,\ which\ state\ \mathrm{long}(p)+c\ belongs\ to.\)
固然,把\(\mathrm{long}(p)+c\)換成\(\mathrm{short}(p)+c\)或者其餘字符串都是能夠的,由於咱們知道獲得的結果是同樣的。
接下來,咱們就要由後綴自動機的定義出發,發掘後綴自動機的性質,進而講解後綴自動機的構造算法。
性質1 :\(s_1\)是\(s_2\)的後綴,當且僅當\(\mathrm{endpos}(s_2)\subseteq \mathrm{endpos}(s_1)\),反之則有\(\mathrm{endpos}(s_1)\cap\mathrm{endpos}(s_2)=\emptyset\)。
證實:
首先第一條證實是顯然的,隨着\(s_2\)的出現,它的後綴\(s_1\)也必然跟隨着出現,出現次數不會小於\(s_2\)的出現次數。然而,因爲\(s_1\)比\(s_2\)更短,因此可能會存在\(s_1\)出現了\(s_2\)沒有出現的狀況,故\(\mathrm{endpos}(s_2)\subseteq \mathrm{endpos}(s_1)\),反之也成立。
第二條也很好理解,\(s_1\)不是\(s_2\)的後綴,他們的出現位置必然沒有交集。若是存在交集,能夠直接說明\(s_1\)是\(s_2\)的後綴,而且由上可知,兩個字符串的\(\mathrm{endpos}\)集合是包含關係。
同時,咱們也能夠知道對於狀態\(p\),\(\mathrm{substr}(p)\)中的字符串都是\(\mathrm{long}(p)\)的後綴。
性質2:對於一個狀態\(p\),和\(\mathrm{long}(p)\)的一個後綴\(s\),若是知足\(|\mathrm{short}(p)|\leq|s|\leq|\mathrm{long}(p)|\),則有\(s\in \mathrm{substr}(p)\)。
證實:
由於\(|\mathrm{short}(p)|\leq|s|\leq|\mathrm{long}(p)|\),因此有\(\mathrm{endpos}(\mathrm{short}(p))\supseteq\mathrm{endpos}(s)\supseteq\mathrm{endpos}(\mathrm{long}(p))\),又由於\(\mathrm{endpos}(\mathrm{short}(p))=\mathrm{endpos}(\mathrm{long}(p))\),因此\(s\in \mathrm{substr}(p)\)。
性質3:\(\mathrm{substr}(p)\)包含的是\(\mathrm{long}(p)\)的一系列連續後綴。
證實:
由性質\(2\)不可貴出,而且,這一系列後綴會在先後界的某一個位置斷開,那就是比\(\mathrm{long(p)}\)還長的字符串不屬於這個狀態,比\(\mathrm{short}(p)\)還短的後綴不屬於這個狀態,緣由是比\(\mathrm{long(p)}\)還長的字符串出現次數可能比\(\mathrm{long}(p)\)少了,它們不屬於同一個\(\mathrm{endpos}\)等價類,比\(\mathrm{short(p)}\)還短的後綴出現位置可能比\(\mathrm{short}(p)\)更多了,它們也不屬於同一個\(\mathrm{endpos}\)等價類。
咱們剛纔在前面提到,後綴自動機還有一個獨有的結構就是後綴連接\(\mathrm{Suffix-link}\)。根據上面的性質\(3\),咱們知道\(\mathrm{substr}(p)\)包含的後綴會在某個位置斷開,那麼咱們就能夠定義後綴連接了:若狀態\(q\)知足\(\mathrm{long}(q)+1=\mathrm{short}(p)\),且\(\mathrm{substr}(q)\)都是\(\mathrm{long}(p)\)的後綴,則有\(\mathrm{link}(p)=q\),稱\(p\)有一條到\(q\)的後綴連接。
也就是說,咱們剛纔提到的某個更短的後綴,由於出現位置更多而不在同一個狀態裏的,就由後綴連接這一結構鏈接起來了。而且,由狀態\(p\)出發,一直走後綴連接,把路徑上全部狀態的\(\mathrm{substr}\)集合並起來,獲得的字符串集合就包含了\(\mathrm{long}(p)\)的每個後綴。
根據\(\mathrm{endpos}\)集合的包含關係,後綴連接和後綴自動機的狀態造成了一個內向樹結構,咱們稱其爲\(\mathrm{Parent}\)樹。
因爲\(\mathrm{Parent}\)樹的葉子節點最多有\(n\)個,每一個內部節點至少有兩個兒子,因此後綴自動機最多有\(2n-1\)個狀態,咱們就證實了後綴自動機的狀態數是線性的。這也告訴咱們,寫後綴自動機的時候數組要開到\(2n\)大小。
根據上面的一些結論,咱們能夠證實後綴自動機的轉移數也是線性的。
首先,咱們能夠任意的求出\(\mathrm{SAM}\)的一棵生成樹,它有\(2n-2\)條邊。而後咱們考慮一條非樹邊\((a,b)\),它對應了一個非連續轉移。那麼就必然有一條路徑\((S\rightarrow a)+(a,b)+(b\rightarrow P)\)對應了原串\(s\)的至少一個後綴。因而咱們就能夠說一個後綴只對應一條路徑,而一條路徑對應至少一個後綴,那麼非樹邊的轉移至多就只有\(n\)條,因此轉移數也是線性的。
根據上面的性質,咱們就能夠學習後綴自動機的算法了。根據證實,咱們知道後綴自動機是的狀態數和轉移數都是線性。事實上,後綴自動機有一個\(O(|s|)\)的在線構造算法,能夠動態加字符,就是增量法。
對於線性構造,咱們不能存太多的信息,通常來講,咱們儲存以下幾項就夠了:
\(\mathrm{maxlen}_p\) | \(\mathrm{link}_p\) | \(\mathrm{trans}_p(c)\) |
---|---|---|
狀態\(p\)的最長字符串長度 | 狀態\(p\)的後綴連接指針 | 狀態\(p\)關於字符\(c\)的轉移函數 |
首先,咱們假設一個獲得了一個字符串\(s'\)的後綴自動機,\(|s'|=n\),如今咱們要加一個字符\(c\),獲得\(s'c\)的後綴自動機。
根據後綴自動機的定義,咱們要可以識別字符串\(s'c\)的\(n+1\)個後綴,應該給以前表明字符串\(s'[1,1],s'[1,2],...,s'[1,n]\)的狀態增長對應的轉移。如今,這些字符串就是\(s'c[1,n]\)這一前綴的全部後綴,而\(s'c[1,n]\)對應的狀態就是以前的終止狀態\(last\)。若是找到它的全部後綴對應的狀態呢?咱們剛纔已經說了,一直走後綴連接的路徑就是他每個後綴對應的狀態。
因爲\(s'c\)這整個字符串也是一個後綴,而且必定不能被以前的後綴自動機識別,因此咱們先創建一個狀態\(cur\),表明至少\(s'c\)這一個後綴。
而後咱們遍歷狀態\(last\)到初始狀態\(S\)的後綴連接路徑,更新後綴自動機。
如下,咱們將要分三種狀況討論:
\(\mathrm{Case}1:\)
後綴連接路徑上的全部狀態\(p\)都沒有\(\mathrm{trans}_p(c)\)這個轉移,如今咱們根據後綴自動機轉移函數的定義,把這個轉移補上去,賦值\(\mathrm{trans}_p(c)=cur\)便可。同時,咱們也得知後綴\(s'c[1,n]\)沒有任何一個更短後綴出現位置比\(s'c[1,n]\)更多,那麼賦值\(\mathrm{link}_{cur}=1\)便可。
\(\mathrm{Example1}:\)
例如咱們已經獲得了字符串\(aa\)的後綴自動機,如今要求出\(aab\)的後綴自動機,流程以下:
此時\(last=2,cur=3\),後綴連接路徑就是黃色虛線連接的\(2\rightarrow 1\rightarrow S\),他們都沒有\(b\)這個字符對應的轉移,因此咱們把這個轉移補上,就是紅色的轉移。而後建立節點\(3\)的後綴連接便可。
\(\mathrm{Case2}:\)
後綴連接路徑上存在一個節點\(p\)有\(\mathrm{trans}_p(c)\)這個轉移,設\(\mathrm{trans}_p(c)=q\),知足\(\mathrm{maxlen}_q=\mathrm{maxlen}_p+1\)。
那麼根據\(\mathrm{Suffix-link}\)的定義,咱們知道狀態\(q\)中的字符串都是\(s'c\)的後綴之後,就能夠連接後綴連接了,即\(\mathrm{link}_{cur}=q\)。此時至關於\(n+1\)個後綴,前一部分長度大於\(\mathrm{maxlen}_q\)的由狀態\(cur\)管轄,後一部分長度小於等於\(\mathrm{maxlen}_q\)的由\(q\)及其後綴連接路徑上的其餘狀態管轄。
\(\mathrm{Example2}:\)
例如咱們已經獲得了字符串\(aabb\)的後綴自動機,如今要求出\(aabba\)的後綴自動機,流程以下:
此時\(last=4,cur=6\),後綴連接路徑就是黃色虛線連接的\(4\rightarrow 5\rightarrow S\),其中\(4,5\)都沒有\(a\)這個字符對應的轉移,因此咱們把這個轉移補上,就是紅色的轉移。而後咱們發現狀態\(S\)已經有了\(a\)這個轉移,\(q=\mathrm{trans}_S(a)=1\),知足\(\mathrm{maxlen}_q=\mathrm{maxlen}_p+1\),就是說明\(aabba\)的一個後綴\(a\)在\(aabb\)中就已經出現了,而且狀態\(q=1\)的\(\mathrm{endpos}\)集合包含\(\mathrm{endpos}(cur)\),因此能夠瓜熟蒂落地加上後綴連接\(\mathrm{link}_{cur}=q=1\)。
\(\mathrm{Case3}:\)
後綴連接路徑上存在一個節點\(p\)有\(\mathrm{trans}_p(c)\)這個轉移,設\(\mathrm{trans}_p(c)=q\),不知足\(\mathrm{maxlen}_q=\mathrm{maxlen}_p+1\)。
這種狀況最爲複雜,咱們能夠先看看具體的例子,瞭解究竟是出現了什麼狀況。
\(\mathrm{Example3}:\)
例如咱們已經獲得了字符串\(aab\)的後綴自動機,如今要求出\(aabb\)的後綴自動機,具體狀況以下:
此時\(last=3,cur=4\),後綴連接路徑就是黃色虛線連接的\(3\rightarrow S\),其中\(3\)都沒有\(b\)這個字符對應的轉移,因此咱們把這個轉移補上,就是紅色的轉移。而後咱們發現狀態\(S\)已經有了\(b\)這個轉移,\(q=\mathrm{trans}_S(b)=3\),可是\(\mathrm{maxlen}(q=3)=3\not =\mathrm{maxlen}(p=S)+1=1\),即\(\mathrm{substr(3)}\)除了\(b\)之外還存在更長的串,他們不是\(aabb\)的後綴,那麼咱們就不能直接令\(\mathrm{link}_{cur}=3\)。
這其實代表了\(b\)這個子串又在新的位置出現了,它勢必不能再和\(aab,ab\)屬於同一個狀態,由於他們的\(\mathrm{endpos}\)集合已經不一樣了,不屬於同一個等價類,因此解決方案就是咱們新建一個狀態\(cl=5\),讓\(cl\)管轄原來\(q\)中長度小於等於\(\mathrm{maxlen}_p+1\)那一部分的子串,同時更改\(S\)到\(q=3\)的轉移爲\(S\)到\(cl=5\)的轉移便可。固然,原來\(q\)的轉移\(cl\)應該都有,也就是說要有\(\mathrm{trans}_{cl}(b)=4\)。
那麼如何處理後綴連接呢?其實不難發現,本來\(\mathrm{link}_q=S\)的字符串仍然是\(\mathrm{substr}(cl=5)\)當中字符串的後綴,只需令\(\mathrm{link}_{cl}=\mathrm{link}_q\)便可,如今\(\mathrm{substr}(cl=5)\)中的字符串都是\(\mathrm{substr}(q=3)\)中字符串的後綴,只需令\(\mathrm{link}_{q}=cl\)便可。對於\(cur\)來講,就能夠像第一種狀況同樣令\(\mathrm{link}_{cur} = cl\)了。
大體瞭解瞭解決方案之後,咱們就能夠通常性的概括具體的解決方案了。如圖:
(因爲筆者沒有找到很好的做圖方式,用的都是\(\mathrm{hihocoder}\)的圖,圖中的\(z\)就是上文中的\(cur\),\(u\)就是上文中的\(last\),\(x\)就是上文中的\(q\),\(w-v\)就是上文中發現\(\mathrm{trans}_p(c)=q\)的狀態\(p\),\(y\)就是新建的狀態\(cl\),望讀者見諒)
咱們在遍歷後綴連接路徑時,遇到一部分連續的狀態知足\(\mathrm{trans}_p(c)=\mathrm{NULL}\),只需賦值\(\mathrm{trans}_p(c)=cur\)便可,對於存在\(\mathrm{trans}_p(c)=q\)的點\(p\),而且\(\mathrm{maxlen}_q\not =\mathrm{maxlen}_p+1\),那麼咱們須要新建一個節點\(cl\)管轄長度小於等於\(\mathrm{maxlen}_q+1\)的子串,而後將原後綴連接路徑上知足\(\mathrm{trans}_p(c)=q\)的全部狀態\(p\)的轉移改成\(\mathrm{trans}_p(c)=cl\),而且讓\(cl\)復刻狀態\(q\)的全部轉移,最後鏈接後綴連接\(\mathrm{link}_{cl}=\mathrm{link}_q,\mathrm{link}_q=\mathrm{link}_{cur}=cl\)便可。
至此,構造算法結束。
首先,對於大字符集的問題,咱們能夠用\(\mathrm{map}\)來存儲轉移邊,這樣的話時間複雜度是\(O(n\log n)\),空間複雜度是\(O(n)\)。對於小字符集的問題,咱們能夠用定長數組來存儲轉移邊,這樣時空複雜度均爲\(O(n|\Sigma|)\)。若是用鏈表來優化遍歷的話,那麼時間複雜度就是\(O(n)\)。
以下代碼用結構體封裝了後綴自動機的實現,字符集大小默認\(26\),\(\mathrm{Extend}\)即爲拓展字符函數。
struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; // trans爲轉移函數,link爲後綴連接,maxlen爲狀態內的最長後綴長度 // tot爲總結點數,last爲終止狀態編號 SuffixAutomaton () { last = tot = 1; } // 初始化:1號節點爲S inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; // 建立節點cur for ( p = last; p && !trans[p][c]; p = link[p] ) // 遍歷後綴連接路徑 trans[p][c] = cur; // 沒有字符c轉移邊的連接轉移邊 if ( p == 0 ) link[cur] = 1; // 狀況1 else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; // 狀況2 else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; // 狀況3 memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } last = cur; } };
根據後綴自動機的定義和構造過程,咱們不難發現後綴自動機的狀態和轉移構成了一張有向無環圖,咱們稱之爲\(\mathrm{DAWG}\)。在後綴自動機上,\(\mathrm{DAWG}\)的拓撲序能夠方便的實現動態規劃。如今,咱們將用一種很簡單的基數排序方式求出\(\mathrm{DAWG}\)的拓撲序。
不難發現,一個狀態\(p\)在\(\mathrm{DAWG}\)中的層數就是\(\mathrm{maxlen}_p\),因此咱們能夠用一個桶統計出層數爲\(i\)的節點有幾個,而後求一遍前綴和,就能夠獲得層數小於等於\(i\)的節點有幾個,而後就能夠直接取出編號獲得拓撲序列了。
inline void Topsort(int n) { for (int i = 1; i <= tot; i++) ++buc[ maxlen[i] ]; for (int i = 1; i <= n; i++) buc[i] += buc[i-1]; for (int i = 1; i <= tot; i++) ord[ buc[maxlen[i]]-- ] = i; // ord[i] 表明拓撲序列中第i個點的編號 }
設節點\(p\)的\(\mathrm{endpos}\)等價類大小爲\(size_p\),則有:
\[ size_p=\sum_{\mathrm{link}_q=p}size_q+1 \]
因爲一個節點\(\mathrm{Suffix-link}\)連接的點的拓撲序位置必定小於這個點的拓撲序位置,因此能夠用拓撲序逆序更新,不須要建出\(\mathrm{Parent}\)樹。
for (int i = tot; i >= 1; i--) size[ link[ord[i]] ] += size[ ord[i] ];
例題:\(Luogu\ P3804\)
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 2e6+20; struct SuffixAutomaton { int link[N],maxlen[N],trans[N][26],cnt[N],ord[N],size[N],tot,last; SuffixAutomaton () { tot = last = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[cur] = link[q] = cl; } } size[ last = cur ] = 1; } inline void Topsort(int n) { for (int i = 1; i <= tot; i++) ++ cnt[maxlen[i]]; for (int i = 1; i <= n; i++ ) cnt[i] += cnt[i-1]; for (int i = 1; i <= tot; i++) ord[ cnt[maxlen[i]]-- ] = i; for (int i = tot; i >= 1; i--) size[link[ord[i]]] += size[ord[i]]; } }; SuffixAutomaton T; char s[N]; int main(void) { scanf( "%s" , s+1 ); int n = strlen( s+1 ); for (int i = 1; i <= n; i++) T.Extend( s[i] - 'a' ); T.Topsort( n ); long long ans = 0; for (int i = 1; i <= T.tot; i++) if ( T.size[i] > 1 ) ans = max( ans , 1LL * T.size[i] * T.maxlen[i] ); printf( "%lld\n" , ans ); return 0; }
節點\(p\)的\(|\mathrm{substr}(p)|\)就是其表明的字符串數量,容易得知:
\[ |\mathrm{substr}(p)|=\mathrm{maxlen}_p-\mathrm{maxlen}_{\mathrm{link}_i} \]
因而直接對每個狀態的字符串數量求和便可。
for (int i = 1; i <= tot; i++) ans += maxlen[cur] - maxlen[link[cur]];
例題:\([SDOI2016]\) 生成魔咒
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 2e5+20; struct SuffixAutomaton { map <int,int> trans[N]; long long ans; int link[N],maxlen[N],tot,last; SuffixAutomaton () { tot = last = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p].count(c); p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; trans[cl] = trans[q]; while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } last = cur , ans += maxlen[cur] - maxlen[link[cur]]; } }; SuffixAutomaton T; int a[N],n; int main(void) { scanf( "%d" , &n ); for (int i = 1; i <= n; i++) { scanf( "%d" , &a[i] ); T.Extend( a[i] ); printf( "%lld\n" , T.ans ); } return 0; }
能夠直接把一個字符串放在後綴自動機上遍歷,若是剛好匹配到了終止節點就說明該串是原串的一個後綴。若是匹配到了某個內部節點就說明是原串的一個子串。這樣的話,就能夠實現\(\mathrm{AC}\)自動機的基本內容了。
inline bool Check(string t) { int now = 1 , len = t.size(); for (int i = 0; i < len; i++) if ( trans[now][t[i]-'a'] ) now = trans[now][t[i]-'a']; else return false; return true; }
例題:\([JSOI2012]\) 玄武密碼
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 2e7+20; int id[200]; struct SuffixAutomaton { int trans[N][4],link[N],maxlen[N],tot,last; SuffixAutomaton () { last = tot = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } last = cur; } inline int Query(char *s) { int n = strlen( s+1 ) , p = 1 , res = 0; for (int i = 1; i <= n; i++) if ( trans[p][id[s[i]]] ) p = trans[p][id[s[i]]] , res++; else return res; return res; } }; SuffixAutomaton T; int n,m; char s[N]; int main(void) { freopen( "symbol.in" , "r" , stdin ); freopen( "symbol.out" , "w" , stdout ); id['E'] = 0 , id['S'] = 1 , id['W'] = 2 , id['N'] = 3; scanf( "%d%d" , &n , &m ); scanf( "%s" , s+1 ); for (int i = 1; i <= n; i++) T.Extend( id[s[i]] ); for (int i = 1; i <= m; i++) { scanf( "%s" , s+1 ); printf( "%d\n" , T.Query(s) ); } return 0; } - 6
對於兩個串的最長公共子串,有\(O(n^2)\)的\(dp\)方法,用後綴自動機能夠實現\(O(n)\)。
咱們能夠對於一個串先創建\(SAM\),而後把第二個串放到\(SAM\)上去匹配,若是能夠走轉移邊,那就走轉移邊,匹配長度加一,至關於匹配右端點,反之則跳\(\mathrm{link}\),回到當前匹配串的一個後綴,至關於移動左端點。每次更新完畢後對答案取一個最大值便可。
例題:\(SPOJ\ 1811\)
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 2000020; struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; SuffixAutomaton () { tot = last = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } last = cur; } inline int LCS(char *s) { int n = strlen( s+1 ) , p = 1 , ans = 0 , Ans = 0; for (int i = 1; i <= n; i++) { if ( trans[p][s[i]-'a'] ) p = trans[p][s[i]-'a'] , ans++; else { while ( p && !trans[p][s[i]-'a'] ) p = link[p]; if ( p == 0 ) ans = 0 , p = 1; else ans = maxlen[p] + 1 , p = trans[p][s[i]-'a']; } Ans = max( Ans , ans ); } return Ans; } }; SuffixAutomaton T; int n; char a[N],b[N]; int main(void) { freopen( "lcs.in" , "r" , stdin ); freopen( "lcs.out" , "w" , stdout ); scanf( "%s%s" , a+1 , b+1 ); n = strlen( a+1 ); for (int i = 1; i <= n; i++) T.Extend( a[i] - 'a' ); int ans = T.LCS( b ); printf( "%d\n" , ans ); return 0; }
對於\(n\)個串的最長公共子串,\(SAM\)仍然能夠在\(O(n)\)的時間內求解。對於任意一個串,咱們先創建後綴自動機,而後把其他的串放在上面一次匹配,對每個節點記錄一下可以匹配的最大長度。而後利用拓撲順序更新一下\(\mathrm{Parent}\)樹上祖先的最大匹配長度(一個點可以匹配成功,它的後綴也必定可以被匹配),每一個節點獲得該串的最大匹配長度,再對於不一樣的串之間取\(\min\)便可。
例題:\(SPOJ\ 1812\)
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 200020 , INF = 0x3f3f3f3f; struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],ans[N],mat[N],cnt[N],ord[N],tot,last; SuffixAutomaton () { tot = last = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[cur] = link[q] = cl; } } last = cur; } inline void Topsort(int n) { memset( ans , 0x3f , sizeof ans ); for (int i = 1; i <= tot; i++) ++cnt[ maxlen[i] ]; for (int i = 1; i <= n; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= tot; i++) ord[ cnt[maxlen[i]]-- ] = i; } inline void LCS(char *s) { int len = strlen( s+1 ) , p = 1 , Ans = 0; for (int i = 1; i <= len; i++) { if ( trans[p][s[i]-'a'] ) p = trans[p][s[i]-'a'] , ++Ans; else { while ( p && !trans[p][s[i]-'a'] ) p = link[p]; if ( p == 0 ) p = 1 , Ans = 0; else Ans = maxlen[p] + 1 , p = trans[p][s[i]-'a']; } mat[p] = max( mat[p] , Ans ); } for (int i = tot; i >= 1; i--) { int u = ord[i] , fa = link[u]; mat[fa] = max( mat[fa] , min( mat[u] , maxlen[fa] ) ); ans[u] = min( ans[u] , mat[u] ) , mat[u] = 0; } } inline int Getans(void) { int Ans = 0; for (int i = 1; i <= tot; i++) if ( ans[i] != INF ) Ans = max( Ans , ans[i] ); return Ans; } }; SuffixAutomaton T; char s[N]; int main(void) { scanf( "%s" , s+1 ); int n = strlen( s+1 ); for (int i = 1; i <= n; i++) T.Extend( s[i] - 'a' ); T.Topsort(n); while ( ~scanf( "%s" , s+1 ) ) T.LCS(s); printf( "%d\n" , T.Getans() ); return 0; }
對於字符串\(s\),咱們能夠複製一遍\(s\)而後接在原串後面,獲得\(ss\),而後對於\(ss\)創建後綴自動機。不難發現,在這個後綴自動機上走\(|s|\)步能夠獲得的全部字符串就是\(s\)的全部循環串,只要走字典序最小的轉移邊便可。
例題:\(POJ\ 1509\)
\(\mathrm{Code}:\)
#include <iostream> #include <cstdio> #include <cstring> using namespace std; const int N = 100020; struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; SuffixAutomaton () { tot = last = 1; } inline void Reset(void) { for (int i = 1; i <= tot; i++) link[i] = maxlen[i] = 0 , memset( trans[i] , 0 , sizeof trans[i] ); tot = last = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c] ; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[cur] = link[q] = cl; } } last = cur; } inline int Ergodic(int n) { int p = 1; for (int i = 1; i <= n; i++) for (int j = 0; j < 26; j++) if ( trans[p][j] ) { p = trans[p][j]; break; } return maxlen[p] - n + 1; } }; SuffixAutomaton T; char s[N]; int main(void) { int n; scanf( "%d" , &n ); for (int i = 1; i <= n; i++) { scanf( "%s" , s+1 ); int len = strlen( s+1 ); T.Reset(); for (int j = 1; j <= len; j++) T.Extend(s[j]-'a'); for (int j = 1; j < len; j++) T.Extend(s[j]-'a'); printf( "%d\n" , T.Ergodic(len) ); } return 0; }
上文咱們已經提到過如何求每一個點的\(\mathrm{endpos}\)集合大小,也就是對應字符串的出現次數。進一步地,咱們能夠在\(\mathrm{DAWG}\)上\(dp\),求出通過每個點的子串數量。而後咱們從初始狀態開始\(dfs\),按字典序訪問轉移邊,若是訪問下一個節點的子串總數都不到當前的\(k\)的話,就把\(k\)減掉子串數,而後跳過這條轉移邊,反之則向下訪問便可。每到一個節點記得減掉當前節點的子串數。
若是本質相同也能夠,那麼\(dp\)數組的初值就是\(\mathrm{endpos}\)集合的大小,若是求本質不一樣,那麼\(dp\)數組的初值是\(1\)。
例題:\([TJOI2015]\) 弦論
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 1e6+20; struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; long long f[N]; int buc[N],ord[N],size[N]; SuffixAutomaton () { tot = last = 1; } inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } size[ last = cur ] = 1; } inline void Topsort(int n) { for (int i = 1; i <= tot; i++) ++buc[ maxlen[i] ]; for (int i = 1; i <= n; i++) buc[i] += buc[i-1]; for (int i = 1; i <= tot; i++) ord[ buc[maxlen[i]]-- ] = i; for (int i = tot; i >= 1; i--) size[ link[ord[i]] ] += size[ ord[i] ]; } inline void DynamicProgram(int t) { for (int i = 1; i <= tot; i++) f[i] = ( size[i] = ( t ? size[i] : 1 ) ); f[1] = size[1] = 0; for (int i = tot; i >= 1; i--) for (int j = 0; j < 26; j++) f[ord[i]] += f[ trans[ord[i]][j] ]; } inline void Dfs(int x,long long k) { if ( k <= size[x] ) return void(); k -= size[x]; for (int i = 0 , y; i < 26; i++) if ( y = trans[x][i] ) if ( k > f[y] ) k -= f[y]; else return putchar(i+'a') , Dfs(y,k); } }; SuffixAutomaton T; char s[N]; long long t,k; int main(void) { freopen( "string.in" , "r" , stdin ); freopen( "string.out" , "w" , stdout ); scanf( "%s" , s+1 ); scanf( "%lld%lld" , &t , &k ); int n = strlen( s+1 ); for (int i = 1; i <= n; i++) T.Extend( s[i] - 'a' ); T.Topsort(n); T.DynamicProgram(t); if ( T.f[1] < k ) puts("-1"); else T.Dfs( 1 , k ); return 0; }
後綴樹指的是將一個字符串的全部後綴插入到一個\(\mathrm{Trie}\)樹中,把沒有分叉的邊壓縮後獲得的樹。後綴樹的節點數和邊數仍然是線性的,以下圖所示:
後綴數組則是兩個線性數組\(\mathrm{sa,rank}\),分別表示將字符串\(s\)的\(n\)個後綴,按照字典序排序後排名爲\(i\)的後綴和後綴\(i\)的排名。
咱們固然不會從頭開始介紹後綴樹,可是,咱們能夠經過咱們已經學會的後綴自動機來構造一棵後綴樹。
定理: 反串後綴自動機的\(\mathrm{Parent}\)樹和原串的後綴樹同構。
咱們固然能夠直接記住這個定理,也能夠經過後綴自動機和後綴樹的性質來理解它。上文一直提到,後綴自動機上的一個節點表明了原字符串上的一個\(\mathrm{endpos}\)等價類,也叫\(Right\)等價類。而從後綴樹的壓縮過程來看,壓縮的都是左端點相同的子串。事實上,後綴樹的每個節點都表明了一個\(Left\)等價類。那麼這樣看來,反串的\(\mathrm{Parent}\)樹就是後綴樹是否是極其天然呢?
至於代碼實現,咱們只要倒序將字符串插入後綴自動機便可。
可是還有一個問題,咱們要處理後綴樹邊上的字符串。咱們不妨畫一個圖來看看:
對於字符串\(s\)的兩個後綴,咱們不妨假設他們有一段公共部分,而且得知在\(\mathrm{SAM}\)當中\(\mathrm{Suffix2}\)對應的狀態的\(\mathrm{Parent}\)樹父親是\(\mathrm{Suffix1}\)對應的狀態(\(\mathrm{Suffix1}\)是\(\mathrm{Suffix2}\)的一個前綴,它的出現位置比\(\mathrm{Suffix2}\)更多了)。那麼,在後綴樹裏,咱們知道淺紫色部分是要被壓縮掉的邊,而這兩個節點所鏈接的邊表明的字符串的首字符就是\(\mathrm{Suffix2}\)淺紫色部分後的第一個字符,不妨假設\(\mathrm{Suffix2}\)對應節點插入時的位置爲\(pos\),那麼咱們剛纔所說的首字符就應該是\(s[pos+\mathrm{maxlen}_{\mathrm{link}_i}]\)。固然,這個字符串的長度就是\(\mathrm{maxlen}_i-\mathrm{maxlen}_{\mathrm{link}_i}\)。
儘管圖中的狀況是較爲特殊的,\(\mathrm{Suffix1}\)這個後綴是\(\mathrm{Suffix2}\)這個後綴的前綴,可是比較容易理解,而且通常狀況下其實也是同樣的,這不過這時後綴樹上連接的父親不是真實的後綴節點,而是後綴自動機上的虛點\(cl\)罷了。
這樣創建後綴樹的時間複雜度和創建後綴自動機的時間複雜度是同樣的。
仔細思考一下,後綴樹本質上仍是一顆\(\mathrm{Trie}\)樹的"壓縮版本",那麼如何求得\(\mathrm{sa}\)數組呢?直接按照字典序在後綴樹上\(\mathrm{dfs}\)一遍就能夠了,這就至關於了後綴排序。既然\(\mathrm{sa}\)數組已經求得了,那麼\(\mathrm{rank}\)數組也能夠輕鬆獲得,時間複雜度爲\(O(n|\Sigma|)\)。
例題:\(UOJ\ 35\)
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 2e5+20; struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; int id[N],flag[N],trie[N][26],sa[N],rk[N],hei[N],cnt; // id 表明這個狀態是幾號後綴 , flag 表明這個狀態是否對應了一個真實存在的後綴 SuffixAutomaton () { tot = last = 1; } inline void Extend(int c,int pos) { int cur = ++tot , p; id[cur] = pos , flag[cur] = true; maxlen[cur] = maxlen[last] + 1; for ( p = last; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , id[cl] = id[q] , link[q] = link[cur] = cl; } } last = cur; } inline void insert(int x,int y,char c) { trie[x][c-'a'] = y; } inline void Build(char *s,int n) { for (int i = n; i >= 1; i--) Extend( s[i]-'a' , i ); for (int i = 2; i <= tot; i++) insert( link[i] , i , s[ id[i] + maxlen[link[i]] ] ); } inline void Dfs(int x) { if ( flag[x] ) sa[ rk[id[x]] = ++cnt ] = id[x]; for (int i = 0 , y; i < 26; i++) if ( y = trie[x][i] ) Dfs(y); } inline void Calcheight(char *s,int n) { for (int i = 1 , k = 0 , j; i <= n; i++) { if (k) --k; j = sa[ rk[i]-1 ]; while ( s[ i+k ] == s[ j+k ] ) ++k; hei[ rk[i] ] = k; } } }; SuffixAutomaton T; char s[N]; int main(void) { scanf( "%s" , s+1 ); int n = strlen( s+1 ); T.Build( s , n ) , T.Dfs(1); T.Calcheight( s , n ); for (int i = 1; i <= n; i++) printf( "%d%c" , T.sa[i] , " \n"[ i == n ] ); for (int i = 2; i <= n; i++) printf( "%d%c" , T.hei[i] , " \n"[ i == n ] ); return 0; }
上文中提到的後綴自動機都是針對一個字符串創建的後綴自動機,事實上,咱們還能夠針對多個字符串創建以構後綴自動機,識別且僅識別每個串的後綴,這樣的後綴自動機被稱爲廣義後綴自動機。
網絡上流傳着\(4\)種廣義後綴自動機的構建方法,咱們逐個分析:
\(1.\) 把每一個字符串用一個不在字符集裏的字符連接起來(相似於後綴數組的方法),而後把整個串創建後綴自動機。
正確性基本能夠保證,複雜度正確可是時間常數較大,而且要特殊處理連接字符的節點統計等問題,適用範圍有限,代碼實現簡單。
\(2.\) 每次插入完一個字符串後將\(last\)指針重置到初始節點,而後繼續插入字符串。
正確性得不到保證,當加入的字符串可能重複的時候,會存在覆蓋原來狀態的問題,不建議使用。
\(3.\) 將全部字符串創建一棵\(\mathrm{Trie}\)樹,而後以\(\mathrm{Trie}\)上的父親節點做爲\(last\)節點,\(\mathrm{Bfs}\)或\(Dfs\)創建後綴自動機。
正確性能夠保證,可是當用\(Dfs\)創建時,時間複雜度不正確,最壞可達\(O(|s|^2)\),用\(\mathrm{Bfs}\)創建時間複雜度正確,可是必須離線創建,而且時間常數較大。
\(4.\) 採用創建狹義後綴自動機時\(\mathrm{Case3}\)的拆點處理方法,直接創建廣義後綴自動機。
正確性能夠保證,時間複雜度正確,常數較小,能夠在線處理。
那麼,咱們重點來看一下第四種方法。
咱們仍是把若干字符串看成一棵\(\mathrm{Trie}\)來看,那麼\(\mathrm{endpos}\)的概念就擴展到\(\mathrm{Trie}\)樹上的\(\mathrm{endpos}\)。那麼,當咱們逐個插入字符串的字符時,還需額外考慮一種狀況:插入的上一個結點\(last\)後已經有了字符\(c\)這個轉移,假設轉移到\(q\)。
仍然須要分兩種狀況,第一種就是\(\mathrm{maxlen}_q=\mathrm{maxlen}_{last}+1\),也就是說某兩個字符串存在相同的前綴,那麼就代表此次插入確實是沒有必要的,直接跳過便可。第二種和狹義後綴自動機的\(\mathrm{Case3}\)是同理的,就代表在\(\mathrm{Trie}\)樹上這個字符串已經不屬於\(q\)的\(\mathrm{endpos}\)集合了,由於他在其餘分叉上又出現了。那麼,處理方式仍是同樣的,只需創建一個虛點\(cl\)管理長度小於等於\(\mathrm{maxlen}_{last}+1\)的子串便可,剩下的歸還原節點處理,代碼實現是如出一轍的,相信理解了\(\mathrm{Case3}\)的本質之後讀者應該不難理解,這只不過是把\(\mathrm{endpos}\)集合的概念拓展到了\(\mathrm{Trie}\)樹上而已。
例題:\([ZJOI2015]\) 諸神眷顧的幻想鄉
\(\mathrm{Code}:\)
#include <bits/stdc++.h> using namespace std; const int N = 4000020; struct SuffixAutomaton { int trans[N][10],link[N],maxlen[N],tot; SuffixAutomaton () { tot = 1; } inline int Extend(int c,int pre) { if ( trans[pre][c] == 0 ) { int cur = ++tot , p; maxlen[cur] = maxlen[pre] + 1; for ( p = pre; p && !trans[p][c]; p = link[p] ) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } return cur; } else { int q = trans[pre][c]; if ( maxlen[q] == maxlen[pre] + 1 ) return q; else { int cl = ++tot; maxlen[cl] = maxlen[pre] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( pre && trans[pre][c] == q ) trans[pre][c] = cl , pre = link[pre]; return link[cl] = link[q] , link[q] = cl; } } } inline long long Query(void) { long long res = 0; for (int i = 1; i <= tot; i++) res += maxlen[i] - maxlen[link[i]]; return res; } }; struct edge { int ver,next; } e[N]; SuffixAutomaton T; int n,c,t,col[N],id[N],Head[N],deg[N]; inline void insert(int x,int y) { e[++t] = (edge){y,Head[x]} , Head[x] = t; } inline void input(void) { scanf( "%d%d" , &n , &c ); for (int i = 1; i <= n; i++) scanf( "%d" , &col[i] ); for (int i = 1 , u , v; i < n; i++) scanf( "%d%d" , &u , &v ), insert( u , v ) , insert( v , u ), ++deg[u] , ++deg[v]; } inline void Dfs(int x,int fa) { id[x] = T.Extend( col[x] , id[fa] ); for (int i = Head[x]; i; i = e[i].next) { int y = e[i].ver; if ( y == fa ) continue; Dfs( y , x ); } } int main(void) { freopen( "substring.in" , "r" , stdin ); freopen( "substring.out" , "w" , stdout ); input() , id[0] = 1; for (int i = 1; i <= n; i++) if ( deg[i] == 1 ) Dfs(i,0); printf( "%lld\n" , T.Query() ); return 0; }
後綴自動機的入門和基本運用方法到這裏爲止就已經講完了,剩下的就是後綴自動機的高級運用,例如結合線段樹合併等數據結構方法維護更多的信息,結合動態規劃方法進行更多的統計,以及廣義後綴自動機的深刻理解等,後續博客可能還會總結。
<後記>