Text
自动机的概念此处不解释。。
引入
我们需要一种数据结构能够识别一个字符串S的所有后缀
一种方法是直接建一棵Trie树,把每个后缀扔到里面去
这样的时空复杂度都是O(N^2)的,不能满足我们的要求
SAM应运而生
观察建好的Trie树,我们发现有很多的状态是冗余的,利用率低下
核心思想
SAM的思想就是将同一类的状态缩到一个点上
这样原来的Trie就变成了一个DAG
定义集合 Right R i g h t 表示某一个节点所代表的字符串在S中的出现位置的右端点的集合
对于Trie上某一个节点,它所代表的字符串是唯一的,是S的某一个子串。
对于某一个节点记录一个
len
l
e
n
,表示根到这个点的距离,也就是所代表的串的长度。
那么只要有了Right集和len,我们就能获得这个字符串了
就是Right集中的位置向左len个长度
为了方便理解,设Right集合中的元素为
w1
w
1
~
wm
w
m
那么
S[w1−len+1...w1]=S[w2−len+1...w2]=......
S
[
w
1
−
l
e
n
+
1...
w
1
]
=
S
[
w
2
−
l
e
n
+
1...
w
2
]
=
.
.
.
.
.
.
,这就是它所代表的字符串
我们尝试合并状态,尝试将Trie上所有Right集相同的点缩到一起
那么合并以后,一个节点所代表的字符串变成了多个,一个节点的len变成了一个区间,记为 [min,max] [ m i n , m a x ]
这个节点所代表的字符串就是Right集中的位置向左[min,max]这个区间长度所得到的字符串集合
定义 fail f a i l 指针表示Right集包含当前节点的Right集,并且其最小的位置
设当前点为p
那么显然有性质,fail[p]所代表的字符串一定都是p所代表的字符串的后缀
并且
minp=maxfail[p]+1
m
i
n
p
=
m
a
x
f
a
i
l
[
p
]
+
1
所以实际上我们并不需要记录min
画个图很容易发现这是正确的
可以发现fail指针构成了一棵树
合并后的东西就是构造出来的SAM了
SAM的性质
首先显而易见的,这个SAM不仅能识别S串所有的后缀,同时也能识别所有的子串,并且相同的子串不会重复出现
对SAM进行DFS,走过的边组成的字符串就是S的子串,每种走法唯一对应一个子串
其次,节点之间的Right集要么一个是另一个的真子集(在fail树上的祖先),要么就不相交
如果Right集合是相交的,那么一个串必定是另一个串的后缀,那么一定成真子集关系
第三,它的转移边数和节点数都是O(|S|)的
放到fail树上考虑
很明显,因为要么不相交,要么是真子集,那么最坏情况就是叶子节点的Right集只有一个元素,然后每增加一个节点就合并一些
显而易见最多只会有2|S|个节点
同时转移边数也是O(|S|)的
此处不给出证明,有兴趣者可以自行查找。。。
SAM的构造
现在的问题是如何在O(|S|)的复杂度内将这个SAM构造出来
考虑已经构造出了S的一个前缀S1,这个前缀结尾位置为 L L ,的SAM,然后在后面新加入一个字符设为,并在SAM上加入相应的边和点
假设之前走到的最末的一个点为 last l a s t ,最末指的是有一条路径到达last,使得这条路径的字符串对应了前缀S1,显然 Rightlast={L} R i g h t l a s t = { L }
我们需要对所有Right集合中有L这个位置的点进行修改,因为这些点可以走一条c的边进入我们新的状态。
Right集合有L这个位置,那就是last的fail树上到根的路径咯
新加一个点np
那么从last开始在fail指针上跑,如果跑到的点没有c的出边,那就加上,指向np(直到跑到根)
如果当前点(设为p)有一个c的出边了,指向q
那么是不是应该将np的fail指向q呢
不一定
举个例子
S串为BAAAcAAAAc
p所代表的最长的串是AAA,即
maxp=3
m
a
x
p
=
3
分情况讨论
maxp+1=maxq m a x p + 1 = m a x q ,也就是说q代表的最长的串是AAAc,q最多只能从p过来,没有比p过来更长的了,此时发现q的Right集真包含了np的Right集,并且显然 Rightq R i g h t q 是最小的
那么 failp=q f a i l p = qmaxp+1<maxq m a x p + 1 < m a x q ,q代表的最长的串可能是BAAAc,发现它的Right集和np没半毛钱关系
此时可以新建节点nq,将q拆成两部分,一部分就是nq, maxnq=maxp+1 m a x n q = m a x p + 1 ,一部分就是剩下的q,max不变, minq=maxnq+1 m i n q = m a x n q + 1
实际意义就是将长度小于等于AAAc切出来给nq,把长度大于它的留给原来的q
nq最长代表AAAc,q最短代表BAAAc
那么显然 failq=nq f a i l q = n q ,可以发现此时nq和第一条一样了,那么 failnp=nq f a i l n p = n q
同时nq的出边、fail全部复制q的,这也显而易见
然后可以发现现在的p已经不能走到q了,而是走到nq
那么从p开始,沿着p的fail链走,如果c的出边是q的改成nq,直到找到第一个不是的。(上面的肯定都不是了)或者已经到根
那么就构造完毕了
其他的复杂度显然,只需要考虑在fail上跳的复杂度
记住结论就好了,总复杂度仍然是O(N)的
此处不给出证明,有兴趣者可以自行查找其实是博主太弱不会势能分析。。。
模板的话,可以参考http://blog.csdn.net/hzj1054689699/article/details/78743488
广义SAM
就是可以多个字符串的SAM合并成一个
有在线和离线的两种构造方法
离线:
首先将这个这些字符串建成Trie,然后在Trie上BFS,按照这个的顺序向SAM里加,last相应的是Trie上的父亲
为啥不能DFS?好像是说势能分析的时候会出锅
在线:
动态加字符串,加完一个串就把last移回根
然后重新想之前一样构造,需要注意的是,如果我们需要新建的位置已经有点了,那么需要像之前对跑fail链上的时候对max进行讨论,过程和之前新建np的过程几乎完全一样
应用
因为SAM可以识别S的所有字串,所以。。。
最基础的就是求两个串的最长公共子串,这是O(N)的
直接将其中一个建好SAM,用另一个在上面跑,匹配失败就沿着fail跳,当前最长长度就是当前点的max
还可以在SAM上DP,fail树上DP什么的。。。