SAM 基础理论学习笔记

发布时间 2023-08-08 01:27:51作者: tzc_wk

以前没有系统性地写过 SAM 学习笔记,所以现在板子老敲错,现在写一个。


对于给定字符串 \(s\),SAM 是一个能够识别其所有子串的自动机。更具体地,从初始状态到所有状态的路径都是 \(s\) 的一个子串,并且 \(s\) 的所有子串都可以通过初始状态到某个状态的某条路径表示出来。显然 SAM 有 \(n^2\) 的建法,太逊了,考虑如何 \(O(n)\) 地建 SAM。先抛出一些定义:

  • \(\text{endpos}(t)\)\(t\)\(s\) 中所有出现次数的结束位置的集合。
  • \(\text{shortest}(t)\),所有 endpos 与 \(t\) 相同的字符串中长度中长度最小的那个。
  • \(\text{longest}(t)\),所有 endpos 与 \(t\) 相同的字符串中长度中长度最大的那个。
  • \(\text{minlen}(t)\)\(\text{shortest}(t)\) 的长度。
  • \(\text{maxlen}(t)\)\(\text{longest}(t)\) 的长度。

有了这些定义以后我们可以直观地想到:将所有 endpos 相同的状态缩成一个等价类,那么感性地理解这些等价类个数不会太多,因此我们考虑将每个等价类看作一个状态。在进行接下来的讨论之前先抛出一些引理:

  1. 任意两字符串的 endpos 要么包含要么不交,读者自证不难。

  2. endpos 相同的字符串的长度构成一段连续的区间,且较短者永远是较长者的后缀,读者自证不难。

  3. 状态数的上界是 \(2n-1\),这是因为如果我们将区间的包含关系连边会连出一棵树,其叶子节点上界是 \(n\),因此总结点数上界是 \(2n-1\)


引理三为我们将后缀自动机复杂度降到线性埋下了基础。接下来引入另一个定义:

  • 定义一个状态的后缀链接表示,该状态所表示的字符串中,最长的 endpos 不等于该状态的后缀表示的状态的后缀所表示的状态,下文中记作 \(\text{link}(p)\)

定义一个字符串的后缀树\(i\to\text{link}(i)\) 连边后形成的树(为什么是树呢?因为显然在一个字符串开头砍掉若干个字符后它的 \(endpos\) 集合大小肯定是单调不降的,根据引理 \(1\),这张图必然不会成环,因此它是树)

当然,由 link 的定义也可以直接得出一些结论:

  • \(\text{maxlen}(\text{link}(i))+1=\text{minlen}(i)\)
  • 后缀树上从 \(i\) 到根节点的路径本质上是从开头删去字符。
  • 对于一个状态 \(i\),它在后缀树上到根的路径上所有点的 \([\text{minlen},\text{maxlen}]\) 不交且并集为 \([0,\text{maxlen}(i)]\)

上面的讨论是基于后缀树的一些理论,那么我们又该如何将这套理论与自动机的理论结合在一起呢?由于我们将 edp 相同的缩成了一类,所以有关状态和转移的定义也需做相应的修改:

  • 状态:SAM 上的一个状态表示一个 endpos 相同的等价类,即知道当前所在自动机上的节点,就可以知道当前字符串的 edp。
  • 初始状态:空串所表示的状态。
  • 转移:对于一个状态 \(x\),其转移 \(\delta(x,c)\) 表示对于该等价类中的所有字符串,在其末尾加上 \(c\) 之后能够到达的状态,根据该等价类所有字符串相同可知的 endpos 相同可知最多只会到达一个状态,\(\delta(x,c)\) 也就唯一表示了这个状态。

这样我们可以知道,对于一个字符串表示的状态,在其末尾插入字符可以视作在自动机上转移,在其开头插入字符可以视作在后缀树上向其儿子转移。


现在我们已经知道了后缀树与后缀自动机的联系,下面我们要知道如何构建后缀自动机。

假设现在我们已经知道了当前字符串 \(s[1...n]\) 的后缀自动机,我们要在后面 append 某个字符 \(c\),考虑其变化。

首先我们显然可以在每插入一个字符的时候就维护整个串表示的状态 \(x\),首先我们添加转移 \(\delta(x,c)\),并令 \(y\) 为转移到的新节点。考虑加入这个节点会多出哪些变化。显然存在一个最长的后缀 \(s[i...n]\) 满足 \(s[i...n]+c\) 还是 \(s[1...n]\) 的一个子串,对于比这个后缀更长的后缀表示的状态 \(t\),我们添加转移 \(\delta(t,c)=y\),这一部分暴力跳即可。特判掉不存在这样的后缀的情况,此时直接令 \(link(y)\) 为根节点并返回。我们假设 \(s[i...n]\) 表示的状态为 \(p\)\(\delta(p,c)\) 表示的状态为 \(q\)

显然,根据 edp 的定义,\(\text{maxlen}(q)\ge\text{maxlen}(p)+1\),此时我们分两种情况讨论:

  • \(\text{maxlen}(q)=\text{maxlen}(p)+1\),此时并不会多出新的等价类,对于 \(q\) 在后缀树上的祖先节点,它们的 edp 中会多一个 \(n+1\),其余节点的 edp 并不会改变,而根据定义 \(y\) 的后缀链接就是 \(q\),因此我们直接令 \(\text{link}(y)=q\) 然后返回即可。
  • 否则我们发现 \(q\) 等价类所包含的字符串可以分为两类:一类长度 \(\le\text{maxlen}(p)+1\),一类长度 \(>\text{maxlen}(p)+1\),二者几乎相同:由于在它们后面插入任何一个字符以后,所得的的字符串的 edp 都不会包含 \(n+1\),所以它们在 SAM 上的的转移也完全一致,唯独不同之处在于前者的 edp 包含 \(n+1\),而后者不包含,所以原来的等价类需要分裂成两个等价类。那么分裂成两个等价类又会对其他节点的转移产生怎样的影响呢?我们令前者的状态为 \(cl\),后者保持它原来的标号 \(q\) 不变,那么该自动机的 \(\delta\) 和 link 会产生如下变化:
    • \(\text{link}(q)=\text{link}(y)=cl\)\(\text{link}(cl)\) 则等于原先的 \(\text{link}(q)\),这是显然的。
    • 对于原先 \(\text{link}(t)=q\) 的状态 \(t\),它们现在的 \(\text{link}\) 肯定还是 \(q\) 保持不变。因此除了 \(\text{link}(q),\text{link}(y),\text{link}(cl)\) 其他点的 \(\text{link}\) 均不会发生变化。
    • 对于 \(p\) 在后缀树上到根节点的那段路径上 \(\delta(t,c)=q\) 的状态,在这些等价类的所有字符串中加入 \(c\) 后它们的 edp 都会包含 \(n+1\),因此它们的 \(\delta(t,c)\) 都会改为 \(cl\),而对于其他包含 \(c\) 的转移边且 \(\delta(t,c)=q\) 的点,它们在加入字符 \(c\) 后的 edp 都不会包含 \(n+1\),因此它们的 \(\delta(t,c)\) 都不会发生变化。这样我们就直接从 \(p\) 开始跳 \(\text{link}\) 暴力修改它们的 \(\delta(t,c)\) 即可。

时间复杂度可以被证明是线性的。但是限于篇幅原因,这里不证明。

最后,总结几个后缀自动机的技巧:

  • 一个字符串的 endpos 集合:如果表示 \(s[1...i]\) 的状态在该字符串所表示状态的后缀树的子树内,那么该字符串的 endpos 则包含 \(i\),否则该字符串的 endpos 不包含 \(i\)可以使用线段树合并维护
  • \(s\) 的某个子串 \([l,r]\) 所表示的状态:找到 \(s[1...r]\) 表示的状态,倍增找到最浅的的 \(\text{maxlen}\ge r-l+1\) 的点。
  • 该图父亲的 len 值一定比儿子小,因此可以对 len 进行桶排求解 DFS 的顺序,类比 DFS 序倒着遍历在某些卡常题中的作用。