「后綴自動機」學習筆記


定義

一個字符串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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM