后缀自动机学习小记

好像很久没有写blog了…

例题

【SPOJ】 Longest Common Substring II
题目大意:给出N(N <= 10)个长度不超过100,000的字符串,求他们的最长公共连续子串。
限制:
Time limit: 0.236s
Source limit: 50000B
Memory limit: 1536MB

分析

哈希

首先想到用二分+hash,这里不细讲了
但是O( LlogL )的时间过不了这题

考虑更快的数据结构

由于字符串总长可以达到 106 ,不能用后缀数组,考虑 O(N) 的算法

后缀自动机

预备

对于一个自动机A,若它能识别字符串S,则A(S)=True,否则A(S)=False
定义init为初始状态,end为结束状态集合,定义函数trans(s,str)表示状态s加入字符串str后到达的状态

定义Reg(s) 表示对于状态s开始能识别的字符串集合,即满足trans(s,str)∈end的字符串str的集合

定义

待处理字符串为S,下标从0开始
定义S[i,j]表示S的下标i..j-1的字串;Suffix(i)=S[i,|S|];Suf为S的后缀集合,即Suffix(i)∈Suf [0≤i<|S|]
Fac表示S的连续字串集合

S的后缀自动机(SAM)能识别S的所有后缀,但是我们要使它变成线性的,才能实现 O(n)

转变为线性

定义st(str) 表示trans(init,str)

对于字符串a和b,如果b∈Reg(st(a)) ,即a的状态字符串ab能被SAM识别,那么ab是S的一个后缀,b也是一个后缀

所以,对于a=S[l..r] ,a能识别Suffix(r+1)

假设a在S中出现位置集合为{[l1,r1],[l2,r2]……[ln,rn]},Reg(st(a))就是{Suffix(r1),Suffix(r2)……Suffix(rn)}

right(a)={r1,r2……rn},可以看出Reg(ST(a))由right(a)决定
一个显然的性质是,如果b是a的后缀,right(a)∈right(b)
right(s) 表示状态s识别的所有子串的right的并集

对于r∈right(s),只要再给定一个长度len,就可以确定一个子串S[r-len,r]
如果长度l,r是合适的,那么长度m[l≤m≤r]也合适,
即对于任意一个r∈right(s) , right(S[r-m,r])是right(s)的子集
因此这个len是一个区间,记这个区间为[ min(s) , max(s) ]

对于两个right不同的状态a,b,它们的right分别记为ra,rb,如果ra∩rb≠∅,令r∈ra∩rb

由于ra≠rb,它们能确定的子串也不同,len区间[ min(a) , max(a) ]与[ min(b) , max(b) ]也不相交

所以可以令 max(a) < min(b) ,那么right(b)⊊right(a)

因此,对于任意两个right,它们要么不相交,要么一个是另一个的子集

parent树

right集合是构成一棵数的,称其为parent树
假设字符串长度为n,那么parent树的叶子节点只有n个,并保证然后每个非叶子节点至少有2个儿子,那么树的大小是线性的

对于状态s,令fa表示parent树上它的父亲,即它的祖先中子树最小(right最小)的
因为fa的儿子至少有2个,即包含了其它状态s’,满足s’∩s=∅,所以right(fa)∩right(s)=right(s)≠right(fa),并且max(fa)=min(s)-1

边的线性

从上面可以确定状态的线性,现在看边的线性
令c=trans(a,b)[b为字符],如果c不在end里,就不要这条边
如果c∈end,就状态a向状态c连一条标号为b的边

先得到SAM的一个生成树,以init为root,然后考虑非树边的构造:
对于init到一个end状态的一条路径:init到a的路径+(a—>c)+c到一个end状态的路径,这个路径是S的一个后缀
因此,一个非树边可能对应到多个后缀,如果将每个这样的后缀(只)对应上它经过的第一个非树边,那么同时非树边至少被一个后缀所对应。
因为后缀只有n个,所以非树边树不会超过n

构造

假设弄出来字符串T的SAM,在T后面加入字符x,令L=|T|
字符串Tx的后缀即T的后缀+x,所以可以用上次的结果来构造

设一个p,使right(p)={L},然后考虑可以表示T的后缀的节点v1,v2…vk,那么v1=p,为了方便,令vi=parent(vi-1)(parent树上的父节点),那么vk为init,并有right={r1,r2…rn-1,rn==L}
添加了x后,令np表示st(Tx),则right(np)={L+1}
如果从在SAM(T)中vi出发没有标号为x的边,那么v的right集合没有一个i[1≤i< n] 使s[ri+1]==x。
由于v1,v2…的right逐渐变大,对于vp出发有标号为x的边,vp+1…vk也有这样的边
对于vi[i< p],它的right中只有rn是满足S[rn+1]=x的(rn=L),所以让它向np连一条标号为x的边

对于vp,令q=trans(vp,x),显然q的right={r(i)+1}[i< n , s[ri]=x]
如果max(q)==max(vp)+1,就可以直接让parent(np)=q并直接结束:
假设vp…ve是一段有标号为x,指向q的边的v
如果max(q)==max(vp)+1,此时vp+1…ve中转移到q的串必定是p转移到q的串的后缀,所以这些字符串也必定出现在位置L+1(这句话有点难理解),就可以令right(q)=right(q)∪{L+1}
所以当Max(nq)=Max(vp)+1,parent(nq)=parent(q)

如果max(q)≠max(vp)+1,对于出现在vp前面任何一个节点vi,可以证明从vi转移到q的串一定不会出现在位置L+1。 如果还是令right(q)=right(q)∪{L+1},就导致有些转移到q的串,它的right对不上
所以把q拆开,新建nq,前一部分的v的right不变,后一部分v的right要扩充,这样可以直接使right(nq)=right(q)∪{L+1},然后
由于right(q)⊊right(nq),parent(q)=nq,同时parent(np)也指向nq
因为nq代替了q,所以对于vp…ve改指向nq

然后就构出了SAM(Tx)

回到例题

只要把n个字符串弄到自动机里,并在过程中标记一下就可以做出来了,时间复杂度 O(L)

附上sam主要部分的代码
struct SAM
{
    int sum,Max[maxM],Parent[maxM],e[maxM][10];
    void insert(int last,int ch)
    {
        int p=last,np=++sum;
        Max[np]=Max[p]+1;
        for (;p>=0 && !e[p][ch];p=Parent[p]) e[p][ch]=np;
        if (p<0) Parent[np]=0;else
        {
            int q=e[p][ch];
            if (Max[p]+1==Max[q]) Parent[np]=q;else
            {
                int nq=++sum;
                memcpy(e[nq],e[q],sizeof(e[q]));
                Parent[nq]=Parent[q];Max[nq]=Max[p]+1;
                Parent[np]=Parent[q]=nq;
                for (;p>=0 && e[p][ch]==q;p=Parent[p]) e[p][ch]=nq;
            }
        }
    }
}S;

你可能感兴趣的:(博客)