zoukankan      html  css  js  c++  java
  • 「后缀自动机」学习笔记

    定义

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

  • 相关阅读:
    UE4_武器4_武器切换(下)
    UE4_武器3_武器切换(上)
    UE4_武器2_矛的攻击与动画
    UE4_武器1_创建武器
    Java知识体系复习笔记
    今日份学习: 毕设。。。。
    笔记:Spring security认证
    一个项目的记录:多人博客
    今日份学习: Spring
    今日份学习:部署?
  • 原文地址:https://www.cnblogs.com/qixingzhi/p/11046028.html
Copyright © 2011-2022 走看看