「后缀自动机」学习笔记

定义

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

posted @ 2019-06-18 16:40  DennyQi  阅读(1171)  评论(4编辑  收藏  举报