后綴自動機是用於識別子串的自動機。
學習推薦:陳立傑講稿,本文記錄重點部分和感性理解(論文語言比較嚴格)。
刷題推薦:[后綴自動機初探],題目都來自BZOJ。
【Right集合】
后綴自動機真正優於后綴樹的方面在於:結合了有限狀態自動機,從而實現了O(n)的時空復雜度。
trans(s,str)表示s+str到達的狀態。
ST(str)=trans(init,str)即包括了str這一子串的唯一狀態(一個子串只能屬於一個狀態)
定義字符串a在S中出現的右端點位置集合Right(a)=r1,r2...rn。
SAM中一個狀態s的Right集合為Right(s),那么狀態s表示所有[Right(a)=Right(s)的子串a],那么實際上狀態s表示的子串a的長度在一個區間內,記作[Min(s),Max(s)]。字符串越短,Right集合越大。
容易證明對於任意狀態a和b,Right(a)和Right(b)要么包含要么不相交。(很顯然的結論,只是論文的證明比較嚴格化)
★在后綴自動機中,一個狀態s(或稱“節點s”)表示的是Right(a)=Right(s)的若干子串a,其長度區間為[MIn(s),Max(s)],從而達到狀態數O(n)的目的。
Right集合形成的樹形包含關系稱為Parent Tree,樹邊由孩子指向父親,是SAM中的失配邊。
Parent樹從上往下,子串長度擴大,Right集合變小,父節點的Right集合包含子節點的Right集合。
fa=Parent(s)當且僅當fa是使Right(fa)最小且滿足Right(s)⊂Right(fa)的結點,(從兒子到父親,就是子串稍微縮短,Right集合變大)
另有Max(fa)=Min(s)-1,實質上是要求fa和s之間沒有一個Right子集x滿足Right(s)⊊Right(x)⊊Right(fa)。
【線性構造SAM】
線性構造采用增量法,即已知字符串T的SAM(T),L=Len(T),在末尾加入字符x,構造SAM(Tx),轉移如下:
①實邊轉移規則:t=trans(s,x)表示s--->t標號為x的邊,若Right(s)={r1,r2...rn},則Right(t)={ri+1|s[ri+1]=x}。
②虛邊轉移規則:Parent樹邊滿足Right(s)⊊Right(fa),Max(fa)=Min(s)-1(含義如上所述)
加入x,考慮所有Right集合包含L的結點v1,v2...vk,顯然它們在Parent樹上是一條從根到葉子的鏈。
定義葉子結點v1=p=ST(T)(即p代表整個串),Right(p)={L},不妨它們從后代到祖先排為v1=p...vk=root。
同時新建結點np=ST(Tx),Right(np)={L+1}。
考慮節點v,Right(v)={r1,r2...rn=L}
根據轉移規則①,如果除了rn外不存在S[ri+1]=x,那么節點v不存在trans(v,x)。
根據轉移規則②,越往上Right集合逐漸擴大直至節點vp存在trans(vp,x),那么v1~vp-1只須向np連出標號為x的邊(np=trans(v,x)),而vp~vk都已經存在trans(v,x)。
令trans(vp,x)=q,當Max(q)=Max(vp)+1時,只須在q的Right集合中插入L+1。
下面重點討論Max(q)>Max(vp)+1的情況。
eg.
T+x:aaabcaaabcaa+b
vp:aaabcaaabcaa Max(vp)=2
q:aaabcaaabcaa Max(q)=4
根據實邊的轉移規則,實際上節點q不止由vp轉移過來,也由vp在Parent樹上的兒子o轉移過來(多條實邊連入):
o:aaabcaaabcaa Max(o)=3
可見,o才滿足Max(q)=Max(o)+1。trans(vp,b)形成了aab,而trans(o,b)形成了aaab,由於有限狀態自動機的性質Right(aab)=Right(aaab)所以合並成同一個點p。
但是當末尾+b時,我們發現Right(aab)>Right(aaab)也就是出現了新的Right集合aab——Right(aab)=Right(q)+Right(np),所以創造新的節點nq:
nq:aaabcaaabcaab Max(nq)=3
Right(nq)實際上是Right(q)和Right(parent(q))夾出來的新Right集合(aab之前被並入q現在分離出來),並且vp將改為連向nq。
vp的祖先中連向q的部分要改為連向nq,因為這些點連向q說明+x后Right集合=Right(q),那么現在就要連向nq。
線性構造SAM的具體步驟如下:
1.新建np,找到vp,vp之前的點連向np。若不存在vp則fa(np)=-1。
2.若len(q)=len(vp)+1,fa(np)=q,結束。
3.len(q)>len(vp)+1,新建節點nq復制q信息並修改len。
4.從vp到根若連向q改為nq。

void insert_SAM(int c){ int np=++size; t[np].len=t[last].len+1; int x=last; last=np; while(x&&!t[x].t[c])t[x].t[c]=np,x=t[x].fa; if(!x)t[np].fa=root;else{ int y=t[x].t[c]; if(t[y].len==t[x].len+1)t[np].fa=y;else{ int nq=++size; t[nq]=t[y];// t[nq].len=t[x].len+1; t[nq].fa=t[y].fa;t[y].fa=t[np].fa=nq; while(x&&t[x].t[c]==y)t[x].t[c]=nq,x=t[x].fa;// } } } last=size=root=1; for(int i=1;i<=m;i++)insert_SAM(s[i]-'a');
注意:后綴自動機節點數是2n。
【技巧和應用】
后綴自動機沒有高論,本質上僅僅是識別子串的自動機,為了壓縮時空復雜度將Right集合相同的子串集合作為一個節點,然后構造自動機的一般屬性:trans邊(實邊)和fail邊(虛邊)。
1.節點的本質:每個節點是Right集合,是Right集合相同的子串集合。
2.實邊:后接字符。
從x的某個子串后面+一個字母,能夠得到y的某個子串。(所以從根沿子串走能得到子串所在節點)
3.虛邊:前刪字符。(匹配失敗fail)
刪除最少的字符使得Right集合變化。故該串在Parent樹上的祖先都是該串的后綴。
1.Right集合:SAM中每個節點是right集合。
每次的np稱為“關鍵節點”,這個節點的最長字符串是以該Right點結尾的前綴。(它最終不一定是葉子)。
Right集合大小:np值為1,按Parent樹建邊進行dfs統計即可。Right集合大小就是每個狀態代表串的出現次數。
統計Right集合具體的話可以用線段樹合並或set啟發式合並。
(實邊:每個節點x連出實邊y相當於在串s(x)后+x到達另一個節點(right盡可能大),這個到達的節點串的前面可能會延伸。
每一條從根到節點的路徑都是一個子串對應的狀態。虛邊:連接右端點位置相同而長度不同子串,該串在Parent樹上的祖先都是該串的后綴,這就可以用來作為fail樹。)
4.匹配:一個串從自動機根節點往下沿實邊走並cnt++,不能走就沿虛邊fail至能繼續走的點y(cnt=len(y)+1),然后往下走。因為Parent的父親一定是后綴,這樣能識別出這個串和SAM的以每個位置結尾的最長公共子串的節點。這些識別點在Parent樹上的所有祖先節點合起來就是所有公共子串。
例題:【BZOJ】4032: [HEOI2015]最短不公共子串(LibreOJ #2123)
5.后綴樹:對逆序串建立SAM,parent樹就是原串的后綴樹,后綴的LCP就是后綴樹上兩個np的LCA。
6.找到指定串節點:在Parent上從對應右端點位置的“前綴開端節點”倍增到Len合適的位置。
7..子串數:對每個點x,Max(x)-Max(fa(x))得到的就是該點的長度區間即子串數,所有點的子串數就是子串總數。