以下主要来自CLJ的ppt,整理并添加了一些附注(错误讲解)
以下皆为口胡,为了装逼掉RP而搞成高端(?)的形式。
神犇轻喷。
Markdown面对长博文,学校的电脑有点力不从心啊。。
定义1: SAM(s) :表示字符串s的后缀自动机。
定义2:rev(s):逆序的s。
定义3: Reg(s) :从状态 s 开始可识别的所有字符串(到目标状态)。
定义4: trans(s,str) :从状态 s 开始,转移为 str ,到达的状态
定义5: ST(s)=trans(init,s) ,即从初始状态开始,转移为 str ,到达的状态,显然该状态表示了子串 s
定义6: Right 集合:对于子串 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)} 。
前提:对于 a,b∈substr(S) ,若 Right(a)=Right(b) ,有 Reg(ST(a))=Reg(ST(b)) ,也有 ST(a)=ST(b) (我们要状态数最简)。
推论2:任意两个状态的Right集合不同。同一个状态可以表示多个子串,且这些子串的Right集合相同。
推论3:状态的个数=本质不同的Right集合的个数(初始状态这里不计入)。
推论4:某个子串对应的状态的Right集合大小就是子串的出现次数。如果未对应任何状态则不存在该子串。
引理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能表示的所有子串必然是该最长子串的后缀。
定义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:同状态表示的子串的最后一个字符相同。
证明:同状态表示的子串为后缀包含关系。
推论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+1∈Right(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) ,得证。
引理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集合有交集,那么令 r∈Right(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)
定义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+1∈Right(s) ,且 ri∈Right(vi),s[ri]=c ,即 Right(v1)∩Right(v2)∩⋯∩Right(vn)≠∅ ,因此 Right(vi) 存在包含关系,即 vi 间成Parent树链。
引理7:Parent树的节点数不超过 2n−1(n≥3) 。
证明:从推论2,定义6出发。Parent树的叶子节点的Right集合元素为1个,而非叶子节点至少有2个孩子,得证。
引理8:后缀自动机的转移数不超过 3n−3 条。
证明:如果对后缀自动机做生成树(和Parent树无关),树边为2n-2条(树节点为2n-1个),考虑非树边,对于转移 trans(a,c)=b ,构造一个字符串 x+c+y ,使 x 满足 ST(x)=a , trans(b,y)=end ,且转移完全经过生成树,发现这个转移从始态通向终态,即表明该字符串是后缀自动机所识别的后缀。由于后缀数目为n,完整串不为x+c+y,因此非树边数目至多 n−1 条,和为 3n−3 条。
推论X:后缀自动机的状态数为 O(n) ,转移数为 O(n) 。
证明:由引理7和引理8得知。
SAM(s)的Parent树即rev(s)的后缀树。
s的后缀树的转移指针即SAM(s)中的转移。
SAM(s)与rev(s)的后缀树的状态一一对应。
两者可以在 O(n) 时间内相互转化。
通过直观判断可知。详见附录5。具体证明这里省略。
待续。。
以上有一些口胡对下面的构造算法没啥用,权当手工建立后缀自动机时判定正确性的手段。
前面一大篇的性质讨论,现在终于到算法层面了。
考虑每次添加一个字符,维护所有原后缀。
设当前字符串为 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而言)最长的子串,扩展这个子串时,我们不需要考虑 ri−max(q) 之前的字符对于当前状态的影响,是没有问题的,建立转移x->np,令 fa(np)=q 即可。
否则我们就要考虑之前的字符对当前状态的影响了,如果我们强行加入 len(Tx) ,若 s[len(Tx)−max(q)] 与 s[ri−max(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个节点。
令 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 。
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)+1≠max(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构造完成。
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似乎是一个论文的汉化,除了看起来有点烦躁之外挺好的(说得好像我这篇文章看起来就不烦躁似的)。
相关博文