后缀自动机 小结

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

1 前序

定义1 SAM(s) :表示字符串s的后缀自动机。
定义2:rev(s):逆序的s。
定义3 Reg(s) :从状态 s 开始可识别的所有字符串(到目标状态)。
定义4trans(s,str):从状态 s 开始,转移为str,到达的状态
定义5 ST(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) ,有 a b的后缀。
证明:由于a和b可以分别表示为 s[ra|a|,ra) s[rb|b|,rb) ,由于 Right(a)=Right(b) ,因此 ra rb 存在一一对应关系,而 |a|<|b| a b是有共同右端点的s子串,有 a b的后缀。

推论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))

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

推论7 Right(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

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

2.5 集合的从属性

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

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

证明,法2:首先要明确的是任两个状态不可以同时表示一个子串(显然,否则匹配子串的时候该走哪一个状态呢?),也就是状态 u v表示的子串没有交集。设 a b的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序可便捷地查询。

引理6 trans(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)=a trans(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 中的连续的一段,更新这一段的转移 x nq即可。
    从算法的角度看来,状态数显然是线性的,每次添加字符,都会至多增加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)=1 max(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 附录

相关博文

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值