后缀自动机:从入门到放弃
寫在前面
后綴自動機,簡稱SAMSAMSAM,是一種十分優秀的字符串匹(shu)配(ju)算(jie)法(gou)
字符串界的bossbossboss,幾乎可以解決全部正常的字符串題目
至少我前前后后學了一年,聽過444次課,幾度懷疑自己不適合oioioi
請做好心理準備
定義
有限狀態自動機
不管,可以理解為有向圖。
唯一的區別是信息儲存在邊上,每個點有字符集個數的轉移到若干其他點,類比字典樹。
如果對于一個字符串,沿著每個點的轉移走,如果不出界,稱該自動機可以表示這個字符串。
以下將“點”稱為狀態
能干啥
后綴自動機能表示母串的所有子串。
算法流程
首先明確一點:子串規模是O(N2)O(N^2)O(N2)的
所以一個狀態必須表示多個子串
也就是說,我們要定義出等價的子串
對于一個子串SSS,定義endpos(S)endpos(S)endpos(S)為SSS在原串中所有出現的結束位置的集合
每個狀態與一個endposendposendpos集合一一對應。即一個狀態表示一個endposendposendpos集合。
需要注意的是,endposendposendpos是完全虛構的,在代碼中不會出現。
然后可以表示所有endposendposendpos等于它的字符串,稱這些子串為一個endposendposendpos等價類
轉移
此時我們定義轉移為這個類所有串加上這個字符后所在的轉移。
一個類的同一個轉移是相同的,因為向ccc的轉移的本質是當前endposendposendpos整體后移一位的所有ccc的位置。
感性理解。
兩個性質
1.兩個子串S1,S2S_1,S_2S1?,S2?滿足S1S_1S1?是S2S_2S2?的后綴,當且僅當endpos(S2)?endpos(S1)endpos(S_2)\subseteq endpos(S_1)endpos(S2?)?endpos(S1?)
S2S_2S2?出現的地方一定有S1S_1S1?出現,但有S1S_1S1?出現的地方不一定有S2S_2S2?
2.一個等價類中的子串均為該類中最長串的后綴且長度連續
第一個顯然
對于一個串SSS,若有后綴S1S_1S1?長度小于SSS,且S1S_1S1?和SSS是等價類
設S2S_2S2?為長度在它們之間的后綴
有endpos(S)?endpos(S2)?endpos(S1)endpos(S) \subseteq endpos(S_2) \subseteq endpos(S_1)endpos(S)?endpos(S2?)?endpos(S1?)
因為endpos(S)=endpos(S1)endpos(S)=endpos(S_1)endpos(S)=endpos(S1?)
所以endpos(S)=endpos(S2)=endpos(S1)endpos(S) = endpos(S_2) = endpos(S_1)endpos(S)=endpos(S2?)=endpos(S1?)
說明它們之間的串都是一個等價類
Parent鏈
由于類中的長度只有一段,逼死強迫癥
所以我們定義每個狀態SSS的failfailfail指針
滿足endpos(S)∈endpos(fail(S))endpos(S) \in endpos(fail(S))endpos(S)∈endpos(fail(S))
且fail(S)fail(S)fail(S)要盡量靠后
可以理解為:從一個狀態沿failfailfail往上跳,取出該類中的所有串,你將會見證這個串不斷失去第一個字符,不斷變為后綴,最后變成空串。我們稱這條鏈為parentparentparent鏈。
先放個圖,以AABAABAAB為栗子
可能看不出啥,但有個大概印象吧
構造算法
SAM 采用增量算法,即一個一個字符插入
這使得 SAM 擅長處理動態問題
現在假設插入第iii個字符,前i?1i-1i?1個的 SAM 已經建立好
首先,上一個插入的點是整個串所在的狀態,記為ppp
新建一個節點,記為curcurcur。顯然curcurcur最長的長度為當前串的長度。
由于其他子串已經處理了,我們要做的,就是搞出當前串的后綴
此處分333種情況
①最簡單的情況
栗子:AA\texttt {AA}AA插入B\texttt BB
此時curcurcur是{3}\{3\}{3}
由于每個類里的字符串是等價的(感性理解)
我們可以找到舊的串的所有后綴,給它加上新的字符
也就是讓ppp沿著failfailfail不斷跳,令ch[p][S[i]]=curch[p][S[i]]=curch[p][S[i]]=cur
即:原來的所有后綴加上新來的字符就成了新的后綴
最后fail(cur)=1fail(cur)=1fail(cur)=1,完結撒花
②然而這只是最簡單的情況
栗子:AA\texttt {AA}AA插入A\texttt AA
沒區別
咦?已經有轉移了呀
說明什么?
說明現在新串的這個后綴已經在之前的串中出現了
那這個后綴的后綴也一定出現了
(請擺脫這個栗子)
記q=ch[p][S[i]]q=ch[p][S[i]]q=ch[p][S[i]]
上面的話翻譯一下,ppp表示的串+S[i]+S[i]+S[i]已經出現了
而這個玩意就是qqq
……嗎?
ppp的最長串+S[i]+S[i]+S[i]一定在qqq上(定義),但不一定是qqq最長的
先討論是最長的的情況
yyyyyy一下,我們要找的后綴不就是qqq的最長串嗎?
而這個后綴的后綴,也就是我們后面要找的,就在qqq的parentparentparent鏈上
這么說來,我們令fail[cur]=qfail[cur]=qfail[cur]=q就好了
over
③然而還有個最復雜的情況
也就是上面的不是qqq最長的
栗子:AAB\texttt{AAB}AAB加B\texttt BB
走程序
停
此時的qqq有B,AB,AAB\texttt {B,AB,AAB}B,AB,AAB
而我們的curcurcur只想要B\texttt BB和鏈上的東西
怎么辦?
拆了唄
記新建的點為q′q'q′
維護信息
首先curcurcur要的是q′q'q′和qqq祖先上的
然后我們發現qqq和q′q'q′有后綴關系
接下來維護轉移
q和q′q和q'q和q′是同一個分出來的,而它們原來的轉移共同構成了后面的狀態
現在它們拆開了,理應維護好后面
即:將qqq的轉移拷貝給q′q'q′
考慮這樣的事實:在前面某個位置,原來轉移到了這個狀態?,F在這個狀態分了,需要考慮具體轉移到哪一邊。
注意到轉移到q′q'q′而不轉移到qqq,當且僅當這個狀態最長串長小于q′q'q′最長串長。
并且都是qqq去掉末尾的一個字符后的后綴
根據意識流這樣的狀態最后面的滿足的剛好是ppp
而剩下的都在ppp的parentparentparent鏈上
即:跳ppp的parentparentparent鏈,如果有到qqq的轉移,將它改到q′q'q′
因為是后綴,所以一定是S[i]S[i]S[i]的轉移(因為ch[p][S[i]]=qch[p][S[i]]=qch[p][S[i]]=q)
至此,SAM 就構造完畢了
復雜度是O(N)O(N)O(N)的,顯然我不會證
代碼
具體實現的時候,每個節點只記錄最長串的長度lenilen_ileni?
int fa[MAXN],ch[MAXN][26]; int len[MAXN],last=1,tot=1; int siz[MAXN],a[MAXN],c[MAXN]; void insert(int c) {int p=last,cur=++tot;len[cur]=len[last]+1;last=cur;for (;p&&!ch[p][c];p=fa[p]) ch[p][c]=cur;//跳failif (!p) fa[cur]=1;//情況1else{int q=ch[p][c];if (len[p]+1==len[q]) fa[cur]=q;//情況2else//情況3{int clone=++tot;len[clone]=len[p]+1;for (int i=0;i<26;i++) ch[clone][i]=ch[q][i];fa[clone]=fa[q];fa[q]=fa[cur]=clone;for (;ch[p][c]==q;p=fa[p]) ch[p][c]=clone;} } }運用
劈配子串
按照定義,沿著轉移走
最長公共子串
建出SSS的后綴自動機,拿TTT去跑
不斷用lenilen_ileni?更新答案
如果走不動了就跳failfailfail
處理出現次數
對于每一次插入,一個類出現次數增加,當且僅當這是當前的后綴
也就是把這個點的parentparentparent鏈都+1+1+1
顯然會TTT。于是先建完,按長度瞎排個序,倒著往上推。
這樣sizisiz_isizi?表示狀態iii中的一個串的出現次數。顯然它們是一樣的。
應該是用的最多的。
void query() {for (int i=1;i<=tot;i++) c[len[i]]++;for (int i=1;i<=n;i++) c[i]+=c[i-1];for (int i=1;i<=tot;i++) a[c[len[i]]--]=i;for (int i=tot;i>=1;i--) siz[fa[p]]+=siz[p]; }然后你就可以處理各種沙雕的字符串題
本質不同的串的個數
由于一個串只會被表示一遍
我們相當于求所有狀態表示的串的個數之和。
即∑(len[p]?len[fa[p]])\sum( len[p]-len[fa[p]])∑(len[p]?len[fa[p]])
Link Cut Tree維護parent鏈
先寫到這里吧,想到再補。
總結
以上是生活随笔為你收集整理的后缀自动机:从入门到放弃的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Steam如何申诉?Steam申诉流程图
- 下一篇: 回文自动机:从入门到只会打板