后缀自动机 小结

来源:互联网 发布:网络进度计划波浪线 编辑:程序博客网 时间:2024/06/08 06:22

以下主要来自CLJ的ppt,整理并添加了一些附注(错误讲解)
以下皆为口胡,为了装逼掉RP而搞成高端(?)的形式。
神犇轻喷。
Markdown面对长博文,学校的电脑有点力不从心啊。。

1 前序

定义1SAM(s):表示字符串s的后缀自动机。
定义2:rev(s):逆序的s。
定义3Reg(s):从状态s开始可识别的所有字符串(到目标状态)。
定义4trans(s,str):从状态s开始,转移为str,到达的状态
定义5ST(s)=trans(init,s),即从初始状态开始,转移为str,到达的状态,显然该状态表示了子串s
定义6Right集合:对于子串s,出现在母串S中的位置是:[l1,r1),[l2,r2),,[ln,rn),那么Right(s)={r1,r2,,rn}
推论1:若Right(s)={r1,r2,,rn},则Reg(s)={suf(r1),suf(r2),,suf(rn)}

2 Right集合

2.1 集合的唯一性

公理?:对于a,bsubstr(S),若Right(a)=Right(b),有Reg(ST(a))=Reg(ST(b)),也有ST(a)=ST(b)(我们要状态数最简)。
推论2:任意两个状态的Right集合不同。同一个状态可以表示多个子串,且这些子串的Right集合相同。
推论3:状态的个数=本质不同的Right集合的个数(初始状态这里不计入)。
推论4:某个子串对应的状态的Right集合大小就是子串的出现次数。如果未对应任何状态则不存在该子串。

2.2 子串的后缀性质

引理1:两个非空子串a和b且|a|<|b|满足Right(a)=Right(b),有ab的后缀。
证明:由于a和b可以分别表示为s[ra|a|,ra)s[rb|b|,rb),由于Right(a)=Right(b),因此rarb存在一一对应关系,而|a|<|b|ab是有共同右端点的s子串,有ab的后缀。

推论5:如果已知状态s能表示的最长子串,s能表示的所有子串必然是该最长子串的后缀。

2.3 子串的长度性质

定义7:状态s能表示的子串存在长度,定义为[min(s),max(s)]
从引理1的证明中可看出些端倪。注意min(s)不一定为1。
如果长度越来越小,肯定存在一个下限,使得再减小,Right集的元素就会变多。
如果长度越来越长,肯定存在一个上限,使得再增加,Right集的元素就会变少(想想看?)。

定义8:fa(s),满足Right(fa(s))是包含Right(s)的最小集合,即Right(s)Right(fa(a))

推论6max(fa(s))=min(s)1
证明:当表示的子串长度=min(s)1时,Right(s)就发生了扩充,由定义6知,新集合即Right(fa(s))
这相当于,fa(s)表示的子串,是s表示子串的后缀,而且紧接s所表示的子串,存在是s状态以外能表示的最长的后缀。

推论7Right(trans(s,c))Right(trans(fa(s),c))

2.4 状态

引理2:同状态表示的子串的最后一个字符相同。
证明:同状态表示的子串为后缀包含关系。
推论8:若trans(x1,c1)=trans(x2,c2)==trans(xn,cn)=s,有c1=c2==cn
证明:即从其他状态转移一个字符而来,也就是说,指向状态s的边的字符都一样。由引理2可知,到达状态s的所有路径表示出的子串的最后一个字符相同。

引理3:若trans(s,ch)!=null,有trans(fa(s),ch)!=null
证明:由于Right集合沿fa走总是在扩充的。

引理4: 若trans(s,c)=t,s[ri]=c,有ri+1Right(t)
证明:对于Right(s)=r1,r2,,rn,只有s[ri]=c的才符合要求,则t的Right集合就包含s[ri]=c|ri+1

推论9max(t)>max(s)
证明:由于ts的后继,此时max(t)=max(s)+1,然后max(t)=maxmax(t),得证。

2.5 集合的从属性

引理5:两个子串ab(|a|>|b|),要么Right(a)Right(b)=,要么Right(a)Right(b)

证明,法1:若Right(a)Right(b),标明子串ab在某个位置同时结束即作为右端点,这种情况只可能有ba的后缀。因此a出现的地方,b必定出现,有Right(a)Right(b)

证明,法2:首先要明确的是任两个状态不可以同时表示一个子串(显然,否则匹配子串的时候该走哪一个状态呢?),也就是状态uv表示的子串没有交集。设ab的Right集合有交集,那么令rRight(a)Right(b),拥有相同结束位置r,此时的子串都可以看作是[0..r)的后缀,由于子串没有交集,也就是说子串的长度没有交集。所以[min(a),max(a)][min(b),max(b)]无交集,不妨令max(a)<min(b),也就是说a表示的子串长度均小与b,且a的是b的后缀,因此Right(b)Right(a)

3 Parent树(F♂A)

定义9:Parent树,由s->fa(s)组成的一棵树。
发现max(s)随着沿fa移动,越来越小,直到0,而初始状态的范围显然是[0,0]。也就是说,沿着fa移动,总会到达初始状态,即由fa构成边组成的树是以初始状态为根的有向树。
Parent树满足,Right(s)Right(ancestor(s))
由关系树可以保存各点的Right集合而且大小线性,dfs序可便捷地查询。

引理6trans(v1,c)=trans(v2,c)==trans(vn,c)=s,有v1,v2,,vn有顺序地构成Parent树链。
证明:可以配合引理2看。由引理4可得,必存在ri+1Right(s),且riRight(vi),s[ri]=c,即 Right(v1)Right(v2)Right(vn),因此Right(vi)存在包含关系,即vi间成Parent树链。

引理7:Parent树的节点数不超过2n1(n3)
证明:从推论2,定义6出发。Parent树的叶子节点的Right集合元素为1个,而非叶子节点至少有2个孩子,得证。

4 性质

4.1 线性

引理8:后缀自动机的转移数不超过3n3条。
证明:如果对后缀自动机做生成树(和Parent树无关),树边为2n-2条(树节点为2n-1个),考虑非树边,对于转移trans(a,c)=b,构造一个字符串x+c+y,使x满足ST(x)=atrans(b,y)=end,且转移完全经过生成树,发现这个转移从始态通向终态,即表明该字符串是后缀自动机所识别的后缀。由于后缀数目为n,完整串不为x+c+y,因此非树边数目至多n1条,和为3n3条。

推论10:后缀自动机的状态数为O(n),转移数为O(n)
证明:由引理7和引理8得知。

4.2 与后缀树的联系

SAM(s)的Parent树即rev(s)的后缀树。
s的后缀树的转移指针即SAM(s)中的转移。
SAM(s)与rev(s)的后缀树的状态一一对应。
两者可以在O(n)时间内相互转化。
通过直观判断可知。详见附录5。具体证明这里省略。

4.3 与后缀数组的联系

不过好像SA能做的SAM都可以?因此并不想写这方面的东西。。

4.4 其他性质

这里讨论一些与应用相关的性质。

定理1:Right集合总是与其Parent子树中在主链上的状态有关。
推论11:Right集合大小等于其Parent树上子树中在主链上的状态数。
证明:(本证明引用了构造算法)显然只有主链上的状态才会扩充Right集合的元素种类数。因此证明本引理即证明非主链状态不会影响Right集合大小。非主链的状态即拆出的nq点,发现nq点相对于q,Right集合多出了L+1,而np的Parent为nq,Right(np)={L+1},即nq的Right集合实际上未新增Right集合元素。得证。
推论12:Right集合的最值与其Parent树上子树中在主链上的状态有关。
证明:由引理9可知,只有在主链上的状态才会增加元素种类,而新增的元素种类是已知的,即该状态的max值。又Right集合为子树中所有Right集合的并集,得证。

同时在维护SAM状态时,也要注意同时维护其Parent保持性质。

5 构造算法

前面一大篇的性质讨论,现在终于到算法层面了。

5.1 算法推演

    考虑每次添加一个字符,维护所有原后缀。
    设当前字符串为T,新字符为x
    考虑所有表示了T的后缀(也就是原后缀)的节点(满足Right集合包含len(T)):v1,v2,
    上次添加之后的p=ST(T),有Right(p)={len(T)}(由于其表示整个T,所以p能表示的子串只能是T的后缀,因此Right只有一个值即len(T)
    我们在添加了x之后,令np表示ST(Tx)Right(np)={len(Tx)}
    由Parent树,令p以及其祖先:v1=p,v2,,vk=rt
    对于v,有Right(v)={r1,r2,,rn=len(T)}
    由状态第3点可知,存在转移x,那么v以及其祖先的Right集合均含有s[ri]=x|ri。不存在转移呢?说明Right(v)不含这样的ri,祖先可能含有,也可能不含有,我们要建立v的转移x,意味着祖先就必须都有转移x,此时只有rn满足(Tx[lenT]=x),建立trans(v,x)=np即可。如果祖先原来都没有这样的转移,表明x不属于原串,那么fa(np)=rt就很显然了。
    以上是不存在冲突的转移,接下来考虑冲突的转移,考虑序列中第一个存在转移x的状态p;Right(p)={r1,r2,,rn(rn<len(Tx))},令s[ri]=x|ri,q=trans(p,x),那么Right(q)={ri+1},直接向Right(q)加入len(Tx)?如果max(q)=max(p)+1,表明q从p接收了(对于q而言)最长的子串,扩展这个子串时,我们不需要考虑rimax(q)之前的字符对于当前状态的影响,是没有问题的,建立转移x->np,令fa(np)=q即可。
    否则我们就要考虑之前的字符对当前状态的影响了,如果我们强行加入len(Tx),若s[len(Tx)max(q)]s[rimax(q),ri)不一致,这导致了再max(q)下,q所表示的字符串不同,则max(q)就必须变小(至少得小到使最大长度为max(q)时所表示的字符串都是一样的),就会打破我们之前建立的自动机的正确性了。
    考虑拆解状态q,为了避免之前字符对当前状态的影响,我们就把没有影响的max(p)拿给np,即Right(nq)=Right(q){len(Tx)}(Right(np)),max(nq)=max(p)+1,然后Right(q),Right(np)Right(nq),有fa(q)=fa(np)=nq。由于链上所有的祖先的Right集合都要加入len(Tx),因此Right(np)肯定会属于q原来的parent,因此fa(np)=fapre(q)
    发现len(Tx)Right(nq),不影响nq的转移(没有从len(Tx)往后的转移,不存在于原串),因此nq的转移等同于原来的q的转移。
新建了状态nq还有东西要处理,由于我们以nq代替q,因此与q相关联的转移x也要更新,q的所有转移x的出发点组成Parent链,由引理6可知这个Parent链肯定是 v1,v2,,vk中的连续的一段,更新这一段的转移xnq即可。
    从算法的角度看来,状态数显然是线性的,每次添加字符,都会至多增加2个节点。

5.2 描述

p=ST(T),Right(p)={len(T)}
新建np=ST(Tx),Right(np)={len(Tx)}
p的所有没有转移x的祖先v,建立trans(v,x)=np
如果p没有存在转移x的祖先v,令fa(np)=rt
如果存在,对p的第一个存在转移x的祖先p(原来的p没用了,这里重定义一下),令q=trans(p,x),若max(q)=max(p)+1,建立fa(np)=q
否则建立状态q的克隆(parent指针以及转移)状态nq,并fa(q)=fa(np)=nq,并对于trans(v,x)=q的p的祖先v,令trans(v,x)=nq

5.3 代码

void extend(char c) {    int np = ++cnt, p = last; last = np; ma[np] = ++len;    while (p && !trans[p][c]) trans[p][c] = np, p = fa[p];    if (!p) fa[np] = rt;    else {        int q = ch[p][c];        if (ma[p] + 1 == ma[q]) fa[np] = q;        else {            int nq = ++cnt; ma[nq] = ma[p] + 1;            memcpy(trans[nq], trans[q], sizeof trans[q]);            fa[nq] = fa[q]; fa[q] = fa[np] = nq;            while (p && trans[p][c] == q) trans[p][c] = nq, p = fa[p];        }    }}

不自带psmatrix根本没有耐心上图。。
建议看附录7一图流。

这里文字描述一下附录7的一图流的构造过程。。。不懂的可以辅助地看。。不过还是建议在草稿纸上跟着画比较好。
现在我们要构建aabbabd的SAM。
1. 首先有一个初始状态S,max(S)=0
2. 加入字符a,建立新状态1,上次的终态S没有转移,建立trans(S,a)=1,令1的Parent为S,max(1)=1
3. 加入字符a,建立新状态2,上次的终态1没有转移,建立trans(1,a)=2,1的后缀链接S存在转移a,但是由于max(S)+1=max(1),令2的Parent为1,max(2)=2
4. 加入字符b,建立新状态3,上次的终态2以及Parent:1和S均没有转移b,建立转移到3,令3的Parent为S,max(3)=3
5. 加入字符b,建立新状态4,上次的终态3没有转移b,建立;3的后缀链接S存在转移b,而且max(S)+1max(3),因此拆解状态3,建立新状态5,复制3的转移以及Parent,令4和3的Parent指向5,将S的转移b指向5,max(5)=1max(4)=4
6. 加入字符a,建立新状态6,上次的终态4和其后缀连接5没有转移a,建立;5的后缀连接S存在转移a指向1,但是满足max(S)+1=max(1),因此令6的Parent为1,max(6)=5
7. 加入字符b,建立新状态7,上次的终态6没有转移b,建立;6的后缀链接1存在转移b到3,拆解3,建立新状态8,复制3的转移以及Parent,改3和7的Parent为8,将状态1的转移b改到状态8,max(8)=2,max(7)=6
8. 加入字符d,建立新状态9,上次的终态7即其后缀链接8,5,S,建立转移d到9,max(8)=7

SAM构造完成。

6 应用

  1. 判断某串是否是s的子串。dfs一次SAM(s)即可。
  2. 不同子串的个数。由于Right集合的唯一性,因此所有子串在SAM中都是不重复的,因此求出SAM的路径条数即可。要注意的是所有节点都可作为路径的终点,因此dp:d[v]=1+<u,v>E(SAM(s))d[u]
  3. 求第k大子串,以字典序dfs自动机即可从小到大遍历子串,处理出每个状态向后延伸的子串数,O(len(S))遍历即可。
  4. 求某子串在原串中的出现次数。即等于其Right集合的大小,又Right集合的从属性,O(len(S))预处理出SAM(s)以及各状态的Right集合大小,询问即找到对应状态即可。
  5. 某子串第一次出现的位置。相当于求其Right集合的最小值,在构建SAM的时候顺带求出即可。
  6. 某子串所有出现位置,相当于求其Right集合的所有元素,沿Parent树回溯一次即可。
  7. 求最短不为s子串的字符串,发现不存在于SAM中的状态就不为其子串,因此dp:d[u]=1+min<u,v>E(SAM(s))d[v]
  8. 求2串的最长公共子串。fa(s)和fail指针。AC自动机的fail指针指向下一个具有最长公共后前缀的状态,而后缀自动机的Parent指针指向是与其拥有最长公共后缀的的某个状态(因为max(fa(s))=min(s)+1)。这一我们可以在串A的自动机上匹配B,找出A和B的最长公共子串。代码好像和AC自动机差不多?(POJ 2774, SPOJ 1811)
for(i=0;s[i];++i) {    c=s[i]-'a';    while(p&&!trans[p][c])p=fa[p];    if(!p)p=rt,len=0;    else len=min(len,ma[p])+1,p=trans[p][c];}

待续。。。。。
附录6似乎是一个论文的汉化,除了看起来有点烦躁之外挺好的(说得好像我这篇文章看起来就不烦躁似的)。

7 附录

相关博文

  • http://cxjyxx.me/?p=244
  • http://cxjyxx.me/?p=246
  • http://lazycal.logdown.com/posts/195299-suffix-automaton-summary
  • http://lazycal.logdown.com/posts/195354-postfix-automatically-building-tree-as-well-as-the-suffix-array
  • http://blog.163.com/ps_lm/blog/static/20790406120125883433110/
  • (实在找不到原文了) http://www.aiuxian.com/article/p-2504420.html
  • (原文已被百度吞了,后缀自动机构造图流,建议看) http://cache.baiducontent.com/c?m=9d78d513d9d431dc4f9995697b13c0166a4381132ba1d50209d2843897732835506793ac57520770a0d13b275fa0131aacb22173441e3de7c595dd5dddccc36979d430340740d1070f9142a49717389260d601b8f14efaeca774c0f58c92c25750c154077983f7&p=c37fc54ad5c340ec0be29638177a9d&newp=9a57c64ad48917e007bd9b7d0d1794231610db2151d6d15f299bcc&user=baidu&fm=sc&query=%BA%F3%D7%BA%D7%D4%B6%AF%BB%FA&qid=fbaa635f00955bce&p1=10
  • http://blog.sina.com.cn/s/blog_8fcd775901019mi4.html
  • http://fanhq666.blog.163.com/blog/static/8194342620123352232937/
  • http://www.xuebuyuan.com/1155017.html
  • (好像混入了啥奇怪的东西) http://lazycal.logdown.com/posts/194613-netflow
0 0
原创粉丝点击