后缀自动机(SAM)
对OI WIKI进行了抄写删减
定义
字符串 \(s\) 的 SAM 是一个接受 \(s\) 的所有后缀的最小 DFA(确定性有限自动机或确定性有限状态自动机)。
-
SAM是一张有向无环图。节点称作状态,边称作转移
-
图存在一个源点\(t_0\),称作初始状态(即下图红点),其他节点均可从\(t_0\)出发到达
-
转移依赖字符进行
-
存在一个或多个终止状态(下图绿点)
-
在满足以上的同时节点数最少
可看作对字符串所有后缀建Trie树,然后对相同的状态(即下文endpos集合相同)缩点
以aaba为例
以abbb为例
线性构造
先明确一些概念
结束位置endpos
字符串s的任意非空子串t,记\(endpos(t)\)为t在s中的所有结束位置。如abcbc,\(endpos(bc)\)=3,5。
两个子串endpos集合可能相等:\(endpos(t_1)=endpos(t_2)\)。于是将所有子串按照endpos集合分为若干等价类。
SAM中每个状态对应一个或多个endpos相同的子串。加上初始状态,SAM的状态个数等于endpos相同的一个或多个子串组成的集合个数+1。
lemma1
两个非空子串 u 和 w(假设 ∣u∣≤∣w∣)的 endpos 相同,当且仅当字符串 u 是 w 的后缀。
lemma2
考虑两个非空子串 \(u\) 和 \(w\) (假设 \(\left|u\right|\le \left|w\right|\) )。那么要么 \(\operatorname{endpos}(u)\cap \operatorname{endpos}(w)=\varnothing\) ,要么 \(\operatorname{endpos}(w)\subseteq \operatorname{endpos}(u)\) ,取决于 \(u\) 是否为 \(w\) 的一个后缀:
\(\begin{cases}
\operatorname{endpos}(w) \subset \operatorname{endpos}(u) & \text{if } u \text{ is a suffix of } w
\\
\operatorname{endpos}(w) \cap \operatorname{endpos}(u) = \varnothing & \text{otherwise}
\end{cases}\)
lemma3
将一个endpos等价类中所有子串按长度递增排序,长度形成连续段
后缀链接link
对于一个状态p,link指向最近的一个q,\(\operatorname{endpos}(p)\subsetneq \operatorname{endpos}(q)\)
以abcbc为例,link构成一棵树,父亲是儿子的最大后缀
最大长度len
对于一个状态p,其表示的所有子串中最大长度为len,最小长度为minlen
考虑一个子串,其endpos与p有交且长度在minlen到len之间,则这个子串包含在状态p中,印证lemma3
在考虑一个子串,其endpos与p有交且长度小于minlen,则这个子串是p在link上的祖先。于是得到\(minlen[p]=len[link[p]]+1\),所以不需要记录minlen。
在link树上,从根到叶子的一支,其子串的长度是连续的,且上为下的后缀
到现在你可以发现,后缀自动机中的后缀只存于link关系中,而节点存的是子串
过程
一开始只包含一个状态\(t_0\),编号为0,len=0,link=-1
现加入一个字符\(c\)
创建新状态cur,lst为加入上一个的状态,首先就有\(len[cur]=len[lst]+1\),即全串。
然后从lst开始向上跳link,对其添加字符\(c\),即对当前串的所有后缀添加字符\(c\)。若没有\(c\)的转移,就一路添加到\(t_0\),这个字符从没出现过,link=0。
假设现在我们找到了一个状态p,其可以通过字符 \(c\) 转移到状态q,停下。此时p上面的所有状态都有\(c\)的转移。
记向 SAM 中插入当前字符 \(c\) 之前的字符串为 \(s\) 。
转移 \((p,q)\)意味着我们尝试向自动机内添加一个 已经存在的 字符串 \(x+c\) (其中 \(x\) 为 \(s\) 的一个后缀,且字符串 \(x+c\) 已经作为 \(s\) 的一个子串出现过了)。我们要把后缀链接连到一个状态上,且其中最长的一个字符串恰好是 \(x+c\) ,即这个状态的 \(\operatorname{len}\) 应该是 \(\operatorname{len}(p)+1\) 。考虑如果不是,即\(\operatorname{len}(q)>\operatorname{len}(p)+1\),即最长字符串是\(ax+c\),但\(ax\)不是\(s\)的后缀,即\(ax+c\)不是cur的后缀,那么当前link不合法。这种情况下,我们必须通过拆开状态 \(q\) 来创建一个这样的状态。
仍以abcbc为例,此时向abcb加入c
我们复制状态 \(q\) ,产生一个状态 \(\textit{clone}\) ,将 \(\operatorname{len}(\textit{clone})\) 赋值为 \(\operatorname{len}(p)+1\) ,将 \(q\) 的所有转移复制到 \(\textit{clone}\)。我们也将从 \(\textit{clone}\) 出发的后缀链接设置为 \(q\) 的后缀链接的目标,并设置 \(q\) 的后缀链接为 \(\textit{clone}\) 。
将从 \(\textit{cur}\) 出发的后缀链接设置为 \(\textit{clone}\) 。
最后一步我们将一些到 \(q\) 转移重定向到 \(\textit{clone}\) 。我们需要修改哪些转移呢?只重定向相当于所有字符串 \(w+c\) (其中 \(w\) 是 \(p\) 的最长字符串)的后缀就够了。即沿着后缀链接遍历,从结点 \(p\) 直到虚拟状态 \(-1\) 。