定義
一個字符串S對應的后綴自動機(SAM)是一個最小的確定有限狀態自動機(DFA),接受且只接受S的后綴。可以理解為能夠在SAM上找到該串的所有子串,且使得SAM狀態數最少。
狀態
$endpos$集
對於S的一個子串s',endpos(s') 為S中所有s'的結束位置集合。以S="aabbabd"為例,endpos("ab") = {3,6}
$endpos$等價類
如果兩個子串的endpos集相等,就把這兩個子串歸為一類。稱所有endpos集相同的子串為一個endpos等價類。定義一個endpos等價類作為SAM的一個狀態。一個endpos等價類中的串互為后綴且長度連續。可以理解為一段后綴。
后綴鏈接
定義一個狀態(也就是一個endpos等價類)中最長串s1長度為maxlen(簡稱len),最小串s2長度為minlen。s2長度不一定為1,因為s2的后綴的endpos集可能不同於s2本身的endpos集。同時,后者一定屬於前者(仔細思考)。於是我們考慮在這兩個狀態之間建立一種聯系,稱之為后綴鏈接(Suffix Link)。從一個狀態出發不停跳后綴鏈接,相當於不停跳到自己的后綴,最終會跳到初始狀態(空)。我們稱這條路徑為后綴路徑(Suffix Path)。
狀態轉移
注意后綴鏈接不等同於狀態轉移,前者不是一個自動機必須具備的,而后者是。
考慮一個狀態u,如果其中所有串的末尾都加上一個相同字符c,那么應該對應哪個狀態?這些原本的串加上一個相同字符之后,應當全部同時存在於一個新的狀態v中。因此一個狀態能通過一個字符轉移到另一個狀態。記為trans[u][c]
構造后綴自動機
增量法,即考慮已經構建好字符串S(設長度為n-1)的SAM,現在要在S后面加上字符c。也就是說,SAM要新增去識別以這個新增的c為結尾的后綴了。
加入c后,后綴自動機的構造會發生變化。同時endpos發生變化的一定是新串的后綴。
由於新增了一個位置,肯定會多一個endpos集{n},因此新開一個狀態z。
情況一:從las開始一路跳后綴鏈接,一直發現trans[p][c]不存在。
這個情況非常特殊。等價於c是S中沒有出現過的。因此所有后綴的endpos一定都是{n}。一路上的點都連z即可。z狀態包括了以n結尾的所有后綴,因此后綴鏈接為源點。
情況二:后綴鏈接的路上點有存在trans[p][c]!=null的,len(p)+1=len(q)
也就是當前后綴在原串中不僅僅出現n那里一次。設trans[p][c]=q,我們判斷q的len是多少。如果len(p)+1=len(q),它的意義就是q中的串全都是p中的串+c得到的。因此對應的后綴全都在q里。因此直接將z的后綴鏈接設為q即可。此時已經找到了不能表示的最長后綴,直接跳出。
情況三:后綴鏈接的路上點有存在trans[p][c]!=null的,len(p)+1<len(q)
有一部分后綴與當前一樣,但一部分后綴的前面部分並不一樣。也就是說加上c以后,原本q的endpos集一個會多出{n},一個不變。因此就需要把q拆開了。新建一個狀態nq。而這兩個集后面再加一個字符,endpos肯定又一樣了(新后綴再加一個字符,沒這個玩意兒,又回來了)。因此他們的出邊都是q原來的出邊。考慮后綴鏈接。現在有q,nq,fa(q),他們互為后綴關系,又顯然存在len(q)>len(nq)>len(fa(q))。最后z的fa了,顯然是nq。然后再走回去,路上如果存在連着q的,幫他改成nq就行了。這里和情況二是一個道理,一旦不等於q了,就可以結束了。
挺難理解的,自己也沒理解透。
/*DennyQi 2019*/ #include <cstdio> #include <algorithm> #include <cstring> #include <queue> using namespace std; const int N = 2000010; inline int read(){ int x(0),w(1); char c = getchar(); while(c^'-' && (c<'0' || c>'9')) c = getchar(); if(c=='-') w = -1, c = getchar(); while(c>='0' && c<='9') x = (x<<3)+(x<<1)+c-'0', c = getchar(); return x*w; } char s[N]; int n,las=1,cnt=1,ans,cnte,nl,fa[N],son[N][26],len[N],sz[N],head[N],nxt[N<<1],to[N<<1]; inline void SAM_add(int c){ int p = las; sz[las = ++cnt] = 1; len[las] = nl; for(; p && !son[p][c]; p = fa[p]) son[p][c] = las; if(!p){ fa[las] = 1; return; } int q = son[p][c]; if(len[p]+1 == len[q]){ fa[las] = q; return; } len[++cnt] = len[p]+1; memcpy(son[q],son[cnt],sizeof(son[q])); fa[cnt] = fa[q], fa[q] = fa[las] = cnt; for(; son[p][c]==q; p = fa[p]) son[p][c] = cnt; } inline void Tree_add(int u, int v){ to[++cnte] = v; nxt[cnte] = head[u]; head[u] = cnte; } void dfs(int u, int Fa){ for(int i = head[u]; i; i = nxt[i]){ dfs(to[i],u); sz[u] += sz[to[i]]; if(sz[u] != 1) ans = max(ans,sz[u]*len[u]); } } int main(){ // freopen("file.in","r",stdin); scanf("%s",s+1); n = strlen(s+1); for(nl = 1; nl <= n; ++nl) SAM_add(s[nl]-'a'); for(int i = 2; i <= cnt; ++i) Tree_add(fa[i],i); dfs(1,-1); printf("%d",ans); return 0; }
1. 最長公共子串
第二個串直接在第一個串的SAM上走。失配時跳fa,因為既然失配,那么當前這個endpos肯定沒用了,跳到最長的后綴繼續匹配。思想和KMP是一樣的。
2. 多串最長公共子串
一個一個在第一個串的SAM上走。記錄對於每一個結束位置能匹配的最大長度,最后每個位置取min,所有位置取max。值得注意的是一個節點滿足時,所有祖先節點都要滿足,而且不能超過len。
3. 最小表示法問題
復制一遍串接在后面,然后再SAM上貪心就可以了。類似之前01trie樹的做法。
一個難點是需要用一個map來存son。用map有一個好處是memcpy可以不需要,map支持直接復制。son[cnt]=son[q]
后綴自動機好麻煩啊(我好菜啊),還是后綴數組吧QAQ