并没有完全理解,处于只会板子阶段。
SAM 就是后缀自动机,比较为大众所接受的定义是:一个接受 s s s 所有后缀的最小自动机。
什么是自动机
比较通俗的说法是一张 DAG ,有一个起始点,路径上有字符。(定义是什么并不重要)。
SAM 的定义是,从起始点走到每个节点一定是原串的一个子串,并且全部子串都在里面。至于为什么会跟子串问题产生关系,先来看看什么事后缀 trie.
后缀 trie/后缀树
问题:求一个字符串本质不同的子串数量。
解法:后缀 trie,插入 S 的所有后缀,从根节点到 trie 每个节点都是一个子串且互不相同。
证明:这不是显然吗。
很不幸,这玩意 O ( n 2 ) O(n^2) O(n2) 的。
考虑一条链其实可以缩成一个点,并不影响树的形态。这就是后缀树。
但是后缀树怎么建呢。掏出一个 SAM。
SAM
先引入一个叫 e n d p o s endpos endpos 的概念。对于一个字符串 S S S, e n d p o s ( S ) endpos(S) endpos(S) 就是它在原串中所有匹配的串的末尾的位置的集合。
SAM 的每一个点实质上是代表一个 e n d p o s endpos endpos 相同的所有后缀。
那么,容易看出对于两个 e n d p o s endpos endpos 集合,要么包含,要不不交。并且一个 e n d p o s endpos endpos 中小的一定是大的的后缀。
那么我们定义 l e n i len_i leni 表示 SAM i i i 号点代表的最长后缀的长度。既然只有包含关系,我们把每个点的父亲设为包含它 l e n len len 最小的。
考虑增量构造一个 SAM,即每次插入一个字符,然后考虑它的贡献,首先新增一些后缀,比如原来是 aabba
加入一个 a
,新增了 aa,baa,bbaa,abbaa,aabbaa
这些。然后这些肯定有已经出现过的,如果一个出现过,那它所有后缀肯定也都出现过,我们只需要找第一个没出现过的(换而言之,最后一个出现的)。
首先找到上一个对应的 SAM 上的点,开始跳 fa,我们定义 ch[i][c]
表示 SAM 上一点连边情况(类似于 trie),直到跳到一个点它 ch[i][c] 是有值的。
在这里不妨先讨论下跳 fa 的本质,既然完全包含,那么 fa 集合任意字符串一定是儿子集合的后缀。
找到那个点 p p p 之后,分类讨论。
1° 没找着
直接跟 rt 连。
2°
l
e
n
c
h
[
p
]
[
c
]
=
l
e
n
p
+
1
len_{ch[p][c]}=len_{p}+1
lench[p][c]=lenp+1
正好中间没有压缩,直接连 ch[p][c]
3° otherwise
这就比较麻烦了。
c h [ p ] [ c ] ch[p][c] ch[p][c] 不再是它的父亲,而包含了一些杂七杂八的东西,这时候就得把它分裂了。
新建一个节点 n q nq nq,作为 n p , c h [ p ] [ c ] np,ch[p][c] np,ch[p][c] 的父亲。然后继承 c h [ p ] [ c ] ch[p][c] ch[p][c] 和 n p np np 有关的信息。