以下主要来自CLJ的ppt,整理并添加了一些附注(
错误讲解)
以下皆为口胡,为了装逼掉RP而搞成高端(?)的形式。
神犇轻喷。
Markdown面对长博文,学校的电脑有点力不从心啊。。
1 前序
定义1:
SAM(s)
:表示字符串s的后缀自动机。
定义2:rev(s):逆序的s。
定义3:
Reg(s)
:从状态
s
开始可识别的所有字符串(到目标状态)。
定义4:
定义5:
ST(s)=trans(init,s)
,即从初始状态开始,转移为
str
,到达的状态,显然该状态表示了子串
s
定义6:
推论1:若
Right(s)={r1,r2,⋯,rn}
,则
Reg(s)={suf(r1),suf(r2),⋯,suf(rn)}
。
2 Right集合
2.1 集合的唯一性
公理?:对于
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集合大小就是子串的出现次数。如果未对应任何状态则不存在该子串。
2.2 子串的后缀性质
引理1:两个非空子串a和b且
|a|<|b|
满足
Right(a)=Right(b)
,有
a
是
证明:由于a和b可以分别表示为
s[ra−|a|,ra)
和
s[rb−|b|,rb)
,由于
Right(a)=Right(b)
,因此
ra
与
rb
存在一一对应关系,而
|a|<|b|
,
a
和
推论5:如果已知状态s能表示的最长子串,s能表示的所有子串必然是该最长子串的后缀。
2.3 子串的长度性质
定义7:状态
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+1∈Right(t)
。
证明:对于
Right(s)=r1,r2,⋯,rn
,只有
s[ri]=c
的才符合要求,则t的Right集合就包含
s[ri]=c|ri+1
。
推论9:
max(t)>max(s)
。
证明:由于
t
是
2.5 集合的从属性
引理5:两个子串
a
和
证明,法1:若
Right(a)∩Right(b)≠∅
,标明子串
a
和
证明,法2:首先要明确的是任两个状态不可以同时表示一个子串(显然,否则匹配子串的时候该走哪一个状态呢?),也就是状态
u
和
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+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个孩子,得证。
4 性质
4.1 线性
引理8:后缀自动机的转移数不超过
3n−3
条。
证明:如果对后缀自动机做生成树(和Parent树无关),树边为2n-2条(树节点为2n-1个),考虑非树边,对于转移
trans(a,c)=b
,构造一个字符串
x+c+y
,使
x
满足
推论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
,新字符为
考虑所有表示了
T
的后缀(也就是原后缀)的节点(满足Right集合包含
上次添加之后的
p=ST(T)
,有
Right(p)={len(T)}
(由于其表示整个T,所以p能表示的子串只能是T的后缀,因此Right只有一个值即
len(T)
)
我们在添加了x之后,令
np
表示
ST(Tx)
,
Right(np)={len(Tx)}
。
由Parent树,令
p
以及其祖先:
对于
v
,有
由状态第3点可知,存在转移
x
,那么
以上是不存在冲突的转移,接下来考虑冲突的转移,考虑序列中第一个存在转移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
所表示的字符串不同,则
考虑拆解状态
q
,为了避免之前字符对当前状态的影响,我们就把没有影响的max(p)拿给
发现
len(Tx)∈Right(nq)
,不影响
nq
的转移(没有从
len(Tx)
往后的转移,不存在于原串),因此
nq
的转移等同于原来的
q
的转移。
新建了状态
从算法的角度看来,状态数显然是线性的,每次添加字符,都会至多增加2个节点。
5.2 描述
令
p=ST(T),Right(p)={len(T)}
。
新建
np=ST(Tx),Right(np)={len(Tx)}
。
对
p
的所有没有转移
如果
p
没有存在转移
如果存在,对p的第一个存在转移x的祖先p(原来的p没用了,这里重定义一下),令
q=trans(p,x)
,若
max(q)=max(p)+1
,建立
fa(np)=q
;
否则建立状态
q
的克隆(parent指针以及转移)状态
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)+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构造完成。
6 应用
- 判断某串是否是s的子串。dfs一次SAM(s)即可。
- 不同子串的个数。由于Right集合的唯一性,因此所有子串在SAM中都是不重复的,因此求出SAM的路径条数即可。要注意的是所有节点都可作为路径的终点,因此dp: d[v]=1+∑<u,v>∈E(SAM(s))d[u]
- 求第k大子串,以字典序dfs自动机即可从小到大遍历子串,处理出每个状态向后延伸的子串数,O(len(S))遍历即可。
- 求某子串在原串中的出现次数。即等于其Right集合的大小,又Right集合的从属性,O(len(S))预处理出SAM(s)以及各状态的Right集合大小,询问即找到对应状态即可。
- 某子串第一次出现的位置。相当于求其Right集合的最小值,在构建SAM的时候顺带求出即可。
- 某子串所有出现位置,相当于求其Right集合的所有元素,沿Parent树回溯一次即可。
- 求最短不为s子串的字符串,发现不存在于SAM中的状态就不为其子串,因此dp: d[u]=1+min<u,v>∈E(SAM(s))d[v]
- 求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