后缀自动机(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等价类中所有子串按长度递增排序,长度形成连续段

对于一个状态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\)

代码

证明

状态数

转移数

操作数(算法复杂度)

posted @ 2023-12-30 18:52  yisiwunian  阅读(19)  评论(0编辑  收藏  举报