字尾自動機只能對乙個字串建立(多個字串需要廣義字尾自動機),處理關於子串、字典序、出現次數等一系列問題的自動機。
字尾自動機的本質是對具有相同資訊的子串(這裡我們把這些子串看作一種狀態)進行壓縮(注意:這裡字尾自動機對所有子串都進行了維護)。並且這個自動機具有圖的結構,用來支援狀態之間的轉移,也就是乙個子串加上乙個字元會到另乙個子串(前提是這個子串要在這個字串中存在),還有會根據子串在原串**現的位置的不同而形成一種樹狀關係(是不是感覺很高階,看不懂,沒關係,我們先了解完演算法之後在去看就會)
也可以新增一些其它資訊(也就是在一開始自動機裡並沒有這些資訊),這些是對字尾自動機能力的擴充套件,不過一般不會有太大的擴充套件。
先給出對字尾自動機最為重要的乙個集合——\(endpos(t)\)。它表示狀態\(t\)在原字串中所有結束位置,例對於\(abcbc\),我們有 \(endpos(bc)=\\)。
我們對具有相同資訊的子串就是根據\(endpos\)的不同而劃分的,我們把\(endpos\)相同的看作為乙個等價類。
為什麼這麼劃分呢,因為它有許多特殊的性質來方便我們處理字串中的子串。
現在這三個性質還只是停留在理論上,我們還不能直接體現在演算法(因為如果你對每乙個維護\(endpos\)的話,空間...)上。所以,乙個重要的來輔助表達出\(endpos\)的陣列——\(link\)鏈結來了。
它是幹什麼的呢?
通過上面的性質,我們已經知道,狀態 \(v\) 對應於具有相同 $endpos $的等價類。我們如果定義 \(w\) 為這些字串中最長的乙個,則所有其它的字串都是 \(w\) 的字尾。
我們還知道字串 \(w\) 的前幾個字尾(按長度降序考慮)全部包含於這個等價類,且所有其它字尾(至少有乙個——空字尾)在其它的等價類中。我們記 \(t\) 為最長的這樣的字尾,然後將 \(v\) 的字尾鏈結連到 \(t\) 上。
這個\(link\)也擁有一些優秀的性質:
到這裡,我們發現\(link\)鏈結的存在,把維護了整個自動機的乙個樹的形態,而結合性質三我們知道每個狀態都可以用乙個長度區間\([x,y]\)表示乙個\(endpos\)等價類(設狀態\(v\)最長的子串為\(s(|s|=y)\)),表示的是\(s[1\sim x],s[1\sim x+1],s[1\sim x+2]...s[1\sim y-1],s\)這些子串,並且\(link[v]\)對應的區間的右端點一定是\(x-1\),所以我們可以就用以下兩個變數大概表示\(endpos\):
這樣通過\(link\)和\(len\),我們能夠表達出該\(endpos\)對應的長度區間\([len[link[v]]+1,len[v]]\)。
這也是字尾自動機對子串壓縮的奧秘,現用endpos表示所有的子串,然後通過\(endpos\)的性質,轉化為一段長度區間,最後通過\(link\)和\(len\)表達出來(所以\(link\)的實質就在這),所以,我們在字尾自動機裡用到\(link\)和\(len\)的時候(雖然有時候\(link/len\)也會單獨運用在題目中),要想到其背後的\(endpos\)。
上面是大段對endpos的解釋,是為了更好理解下面對字尾自動機的構造,我們會通過\(endpos\)用極少的狀態(最大為\(2n-1\))表示所有的子串
在構造中,我們要維護\(link,len,next\)(用來實現狀態之間的轉移)。
這\(n\)個子串中,一定會有子串對應狀態的\(endpos=\\)(至少\(s[1\sim n]\)是),所以這裡加入乙個新狀態,設這個新狀態為\(p\),所以\(len[p]=n\)
如果這\(n\)個子串都沒在原串(\(s[1\sim n-1]\))**現過,對\(endpos[p]\)的更新就沒了。
而如果有一段(\(s[x\sim n]...s[n\sim n]\))已經出現過,那麼我們就要得到表示\(s[x\sim n]\)這個字串的狀態\(y\),並且用這個\(y\)更新\(link[p]\)。
為啥不能直接更新呢?
如果\(len[y]\)就是\(|s[x\sim n]|\),那麼直接更新就可以。否則,因為狀態\(y\)就會有一些字串(就是長度大於\(|s[x\sim n]|\)的這部分),無法在\(n\)出現,即\(endpos\)不相同。這是就要複製\(y\)為\(clone\),其它資訊都一樣,而\(len\)要等於\(|s[x\sim n]|\)。同時,把\(link[y]=clone,link[p]=clone\)。(這裡還要更新\(s[x\sim n-1]\)對應狀態及其祖先的轉移)
那麼我們怎麼得到這個\(y\)呢?
考慮\(s[x\sim n]\)可以拆解為\(s[x\sim n-1]+s[n]\),而\(s[x\sim n-1]\)正是\(s[1\sim n-1]\)的字尾,那麼根據性質六,我們就可以在\(s[1\sim n-1]\)對應的狀態(設為\(last\))向祖先跳,如果存在\(x\in last\)的父親,且\(next[x][s[n]]!=0\)。那麼\(x\)最長的子串就表示\(s[x\sim n-1]\),\(y\)就是\(next[x][s[n]]\)。否則\(link[x]=\)初始狀態。
如果要複製\(y\)之後為啥要更新\(s[x\sim n-1]\)對應狀態及其祖先的轉移呢?
因為\(endpos[clone]\)已經更新了(多了\(\\)),與\(endpos[y]\)不同了,那麼所有本應該轉移到\(y\),且滿足長度小於\(len[clone]-1\)的子串都應該更新為\(clone\),而我們發現這些子串都是在\(x\)或\(x\)的祖先中(就是\(s[x\sim n-1]\)對應狀態及其祖先),更新它們即可。
現在我們講新子串帶來的轉移的更新。
首先,對於\(s[1\sim n],s[2\sim n],s[3\sim n]...s[n\sim n]\)這些子串,可以拆成\(s[1\sim n-1]+s[n],s[2\sim n-1]+s[n],s[3\sim n-1]+s[n]...s[n]\),那麼我們需要更新的轉移,就是\(s[1\sim n-1],s[2\sim n-1],s[3\sim n-1]...s[n-1]\)對應的狀態,那麼我們在順著\(last\)的祖先更新轉移,把所有\(x\in last\)的父親,且\(next[x][s[n]]=0\)的更新。如果是上文提到的已經出現過的一段子串(\(next[x][s[n]]!=0\)),那麼它們的狀態是已經存在的了,那麼就不需它要轉移,結束。
我們可以結合這幾張圖感受一下(黃色邊表示link):
發現得到\(y\)的過程和更新轉移的過程很像,我們可以合併一下,**如下:
void sam(int cc)
if(!x)s[p].li=1;//狀態賦值為初始狀態
else
s[y].li=s[p].li=st;
} }la=p;
}
字尾自動機有什麼用呢?
主要體現在根據\(endpos\)性質,運用\(link/len\)來做事
字尾自動機
基礎知識 step i 表示的是字串i在原字串中的位置。pareint i 表示root到parent i 的子串是root到i的最長字尾。字尾自動機遍歷可以得到原字串的所有子串。特殊技巧 一 字尾自動機的不同子串數有兩種求法 1.ans step i step parent i 1 i cnt 2...
字尾自動機
常用於處理字串問題,可以高效解決許多字串問題。有點像將乙個字串的所有字尾都建在乙個ac自動機上,但不同的是字尾自動機的節點數最多為2 n,因為它只記錄需要記錄的點,一些沒有記錄東西的點可以視為與下面有價值的節點並在一起,這樣大大降低了時間複雜度和空間複雜度。對於每乙個節點記錄它的後面加上每個字元後字...
字尾自動機
基礎學習 簡潔明瞭的講解 總狀態數不超過2n 12n 1 2n 1 包括初始狀態 統計每個end po sendpos endpos 等價類出現位置數量時,要按長度從長到短的計算cnt cntcn t。那為什麼一定要從長到短呢?比如回文自動機就直接是按照節點編號從大到小計算cnt cntcn t 罪...