后缀自动机 (SAM)

后缀自动机

定义

定义 SAM 为一个有限状态自动机,接受且仅接受 \(S\) 的一个后缀。

同时,SAM 是这样的自动机中最小的那个,其中状态数至多为 \(2n - 1\),转移数至多为 \(3n - 4\)

基本性质

  1. SAM 是一张 DAG。
  2. SAM 上从源点 \(t_0\) 出发经过的任意一条路径为原串的一个子串,因此 SAM 上一个节点对应一个子串集合。

单有这些基础性质是不够的,我们可以考虑多寻找一些 SAM 的性质来使用必要条件构造出 SAM。

首先我们需要引入一些强相关定义:

结束位置 endpos

对于字符串 \(s\) 的任意非空子串 \(t\),我们记 \(\mathrm{endpos}(t)\)\(t\)\(s\) 中的所有出现的结尾位置构成的集合。

对于 \(\rm endpos\),我们不加证明地给出如下性质。

  • 对于 \(s\) 的任意两个非空子串 \(t_1, t_2(|t_1| \le |t_2|)\),其中 \(t_1\)\(t_2\) 的一个后缀当且仅当:\(\mathrm{endpos}(t_2) \subseteq \mathrm{endpos}(t_1)\)

  • 对于 \(s\) 的任意两个非空子串 \(t_1, t_2(|t_1| \le |t_2|)\),一定满足:\(\mathrm{endpos}(t_1) \cap \mathrm{endpos}(t_2) = \mathrm{endpos}(t_2) / \varnothing\)

在给出第三条性质之前,我们先定义 \(\rm endpos\) 集等价类,即我们将 \(\rm endpos\) 集相同的子串看作是一个等价类。

  • 对于同一等价类内的子串,其长度连续。

有了关于 \(\rm endpos\) 集的性质之后,我们发现对于 SAM,需要满足如下性质:

  • SAM 上任意一个节点对应子串的 \(\rm endpos\) 集相同。

对于任意的节点 \(x\),我们考虑其表示的任意一个子串 \(t\) 到经过其的后缀的结尾距离构成的集合 \(S\)
不难发现就是从 \(x\) 开始往后遍历到所有终止态经过的节点数构成的集合。
该集合的构成显然与 \(t\) 无关,仅与 \(x\) 有关因此该性质成立。

  • SAM 上不存在两个节点 \(u, v\) 使得 \(u, v\)\(\rm endpos\) 集相同。

反证法:若存在则可以将两个节点合并(转移也合并),SAM 减小与最小性不符。

由此,我们可知 SAM 上的每个节点唯一对应原串的一个等价类。

对于字符串 \(s\) 的任意一个子串 \(t\),定义后缀 \(\mathrm{link}_t\)\(t\) 最长的后缀使得其与 \(t\)\(\rm endpos\) 集不同,特别地,若不存在则将 \(\mathrm{link}_t \leftarrow 0\)

容易得知,根据后缀 \(\rm link\) 连边,我们可以得到一颗树,这颗树称为 \(\rm parent\) 树。

但这个信息量是巨大的,我们不妨使用 SAM 的结构和 \(\rm endpos\) 的性质压缩这个结构。

  • 根据 \(\rm endpos\) 集的定义,同一个等价类中子串的后缀 \(\rm link\) 是相同的。

因此我们可以将所有子串压缩为等价类一个节点,而将连边改为等价类之间的连边,从直观上来看,这样大大减少了连边数。

压缩之后为了储存信息,根据等价类的性质 \(3\),我们只需储存当前等价类内子串的最长长度 \(len\),和最短长度 \(minlen\) 即可。

同时,后缀 \(\rm link\) 还满足如下性质:

  • 对于字符串 \(s\) 的任意子串 \(t_1\),对于另一个子串 \(t_2\),其中 \(t_2\)\(t_1\)\(\rm parent\) 树上的祖先当且仅当 \(t_2\)\(t_1\) 的一个后缀。

  • \(\rm parent\) 树的节点数至多为 \(2n - 1\),即等价类 / SAM 上的节点数至多为 \(2n - 1\)

考虑从上往下暴力构造一颗 \(\rm parent\) 树。
具体地,我们从根节点 \(t_0\) 出发,特别地,我们令此时的 \(\rm endpos\) 集为全集,同时维护一个字符串,初始时为空串。
每次我们在当前的的字符串前加上一个字符 \(c\),计算该字符串的 \(\rm endpos\) 集。
我们扣掉 \(\rm endpos\) 集为空集的字符串,若剩下的字符串 \(\rm endpos\) 集只存在一种,那么显然这些字符串与当前字符串的 \(\rm endpos\) 集相等,可以直接将这些字符串归为同一个等价类,此时节点数不变。
若剩下的字符串存在至少两种 \(\rm endpos\) 集,那么按照 \(\rm endpos\) 集的不同进行划分新建节点将这些节点连向当前节点。对这些新的节点递归操作。
我们只关心新建的节点总数量,可以发现这个流程可以抽象为将一个大小为 \(n\) 的集合不断划分至大小为 \(1\) 的集合,划分的次数即为新建的节点数量。
对于这个划分数量的最大值可以简单地得到一个 \(2n - 2\) 的上界,加上源点总共至多 \(2n - 1\) 个节点。

SAM 的构造

一般地,考虑增量法。令当前字符串为 \(s\),位置为 \(i\),需要往后加一个字符 \(c\)

首先对于 \(s + c\) 的后缀,\(\rm endpos\) 集一定存在 \(i + 1\),且之前的所有 \(\rm endpos\) 都不存在 \(i + 1\),因此一定要新建节点 \(new\)

为了求得 \(s + c\) 的所有后缀,我们可以遍历 \(s\) 的所有后缀然后在其后添加字符 \(c\)

由此,我们可以借助 \(\rm parent\) 树来遍历 \(s\) 的所有后缀,同时,我们也需要维护出新建节点的后缀 \(\rm link\)

此时我们发现,对于 \(s\) 的后缀,一定满足:

  • 存在一个分界点使得前一部分后缀满足 \(+c\) 后在 \(s\) 中不出现,后一部分后缀满足 \(+c\) 后在 \(s\) 出现。同时该分界点在两个等价类之间。

因此对于分界点以前的节点,显然 \(+c\)\(\rm endpos\) 集中只存在 \(i + 1\),因此可以直接在 SAM 上向 \(new\) 连边。

特别地,若不存在一个后缀满足 \(+c\) 后在 \(s\) 中出现,我们将 \(\mathrm{link}_{new} \leftarrow 0\),接下来将不会考虑这种情况。

我们找到第一个等价类使得其 \(+c\) 后在 \(s\) 中出现,令该节点为 \(p\),找到 \(+c\) 的串在原 \(s\) 当中的等价类 \(q\)

此时我们有如下性质:

  • \(len_q \ge len_p + 1, minlen_q \le minlen_p + 1\)

我们可以发现,此时 \(q\) 这个等价类当中长度为 \((len_p + 1, len_q]\) 的子串 \(\rm endpos\) 集中将不存在 \(i + 1\),而长度为 \([minlen_q, len_p + 1]\) 的子串 \(\rm endpos\) 集中包含 \(i + 1\)

(特别地,若 \(len_q = len_p + 1\) 我们最后考虑)

由此,我们需要将 \(q\) 分裂为两个等价类,我们新建节点 \(clo\),令它表示后者构成的等价类,那么需执行如下操作:

先将 \(q\) 的全部信息复制给 \(clo\),然后执行如下操作:len[clo] = len[p] + 1, link[q] = clo

由于等价类已经改变,因此之前连向 \(q\) 的转移需要重新定向。

显然需要重新定向的只有 \(p\) 的祖先节点当中有向 \(q\) 转移的节点,需要把它们都改为 \(clo\)

同时,我们需要把 \(new\) 的后缀 \(\rm link\) 指向 \(clo\)

最后,如果 \(len_q = len_p + 1\),那么 \(q\) 中所有的子串 \(\rm endpos\) 都会加上 \(i + 1\),我们只需要修改 \(new\) 的后缀 \(\rm link\) 指向 \(clo\)

正确性证明

容易发现,除了最小性以外,开始提到的 SAM 需要满足的所有性质都能很容易地利用构造 SAM 的流程证明正确性。

而最小性的证明理论比较复杂,这里不涉及。

复杂度证明

状态数

在后缀 \(\rm link\) 中我们已经使用了一种与 SAM 构造无关的方式证明了一个上界为 \(2n - 1\)

同时,通过 SAM 的构造,我们可以轻松地看出 SAM 的状态数也即等价类数量的一个上界为 \(2n - 1\)

转移数

我们令 \(last\) 表示考虑完 \(s\) 构造的 SAM 中 \(s\) 这个整串对应的状态。

我们发现,每次向 \(new\) 连边,\(last\) 会一直跳后缀 \(link\),最后停下的位置为 \(top\) 那么其一定满足:

  • \(minlen_{clo} \le minlen_{top} + 1\)

\(top\)\(lst\) 这个位置跳过来。

  • \(minlen_{new} \le minlen_{lst} + 1\)

考虑令势能函数 \(\varphi(x) = minlen_x\)

\[\varphi(last) - \varphi(lst) + 1 \le \varphi(last) - \varphi(new) + 2 \]

于是总连边量(不包括复制连边)为:

\[\sum \varphi(last) - \varphi(new) + 2 = 2n - \varphi(new_n) \le 2n - 1 \]

复制连边的复杂度证明好像假了,先鸽。。。先记着总转移数大小的结论:不超过 \(3n - 4\).

复杂度

首先,除了重定向部分,其余部分的复杂度与状态数和转移数相同,因此只考虑重定向部分的复杂度。

令势能函数 \(\lambda(x) = \mathrm{len}(\mathrm{link}(\mathrm{link}(x)))\),那么重定向的复杂度有上界:\(\varphi(top) - \lambda(new) \le \varphi(last) - \lambda(new)\).

更近一步地,将 \(last\) 初次跳两次后缀 \(\rm link\) 的复杂度单独写开:\(2 + \lambda(last) - \lambda(new)\) 那么有总复杂度:

\[\sum\limits 2 + \lambda(last) - \lambda(new) \le 2n \]

因此构建 SAM 的复杂度是线性的。

广义后缀自动机

定义

广义后缀自动机是一个 DFA 接受且仅接受给定的多个模式串 \(s_1, s_2, \cdots s_m\) 其中任意一个的任意后缀。

同样的,广义后缀自动机也满足最小性,核心构造思路(\(endpos\) 集等价类合并压缩)与 SAM 完全一致,性质为 SAM 扩展到多模式串层面,类似于 KMP 自动机和 AC 自动机之间的关系。

构造方式

首先类似于 AC 自动机的构建方式,我们将给定的 \(m\) 个模式串建出 trie 树。

然后按照 bfs 序依次建立,这时与 SAM 建立时满足的性质完全相同。

复杂度

状态数和转移数上界与 SAM 中相同,只与 trie 树大小相关。

重定向部分利用与 SAM 重定向部分一样的势能分析方法,复杂度也是线性的。

posted @ 2021-06-26 12:26  Achtoria  阅读(1125)  评论(0编辑  收藏  举报