后缀自动机(SAM)

本文大部分内容来自于 hihoCoder,侵删。
本文只是将其用更好的格式进行展现,希望对读者有帮助。

定义

后缀自动机(\(\text{Suffix Automaton}\),简称 \(\text{SAM}\))。对于一个字符串 \(S\),它对应的后缀自动机是一个最小的确定有限状态自动机(\(\text{DFA}\)),接受且只接受 \(S\) 的后缀。

比如对于字符串 \(S = \underline{aabbabd}\),它的后缀自动机是

14807564643248.png

其中 红色状态 是终结状态。你可以发现对于 \(S\) 的后缀,我们都可以从 \(S\) 出发沿着字符标示的路径(蓝色实线)转移,最终到达终结状态。

特别的,对于 \(S\) 的子串,最终会到达一个合法状态。而对于其他不是 \(S\) 子串的字符串,最终会“无路可走”。

\(\text{SAM}\) 本质上是一个 \(\text{DFA}\),\(\text{DFA}\) 可以用一个五元组 <字符集,状态集,转移函数、起始状态、终结状态集>来表示。至于 绿色虚线 那些虽然不是 \(\text{DFA}\) 的一部分,但却是 \(\text{SAM}\) 的重要部分,有了这些链接 \(\text{SAM}\) 是如虎添翼,这些后面将再细讲。

下面先介绍对于一个给定的字符串 \(S\) 如何确定它对应的 状态集 和 转移函数 。

SAM 的状态集 (States)

首先先介绍一个概念 子串的结束位置集合 \(endpos\)

对于 \(S\) 的一个子串 \(s\),\(endpos(s) = s\) 在 \(S\) 中所有出现的结束位置集合。

以字符串 \(S = \underline{aabbabd}\) 为例

状态 子串 \(endpos\)
\(S\) \(\varnothing\) \(\{0,1,2,3,4,5,6\}\)
\(1\) \(a\) \(\{{1,2,5}\}\)
\(2\) \(aa\) \(\{2\}\)
\(3\) \(aab\) \(\{3\}\)
\(4\) \(aabb,abb,bb\) \(\{4\}\)
\(5\) \(b\) \(\{3,4,6\}\)
\(6\) \(aabba,abba,bba,ba\) \(\{5\}\)
\(7\) \(aabbab,abbab,bbab,bab\) \(\{6\}\)
\(8\) \(ab\) \(\{3,6\}\)
\(9\) \(aabbabd,abbabd,bbabd,babd,abd,bd,d\) \(\{7\}\)

我们把 \(S\) 的所有子串的 \(endpos\) 都求出来。如果两个子串的 \(endpos\) 相等,就把这两个子串归为一类。最终这些 \(endpos\) 的等价类就构成了 \(\text{SAM}\) 的状态集合。

性质

  1. 对于S的两个子串 \(s1\) 和 \(s2\),不妨设 \(|s1| \le |s2|\),那么 \(s1\) 是 \(s2\) 的后缀当且仅当 \(endpos(s1) \supseteq endpos(s2)\),\(s1\) 不是 \(s2\) 的后缀当且仅当 \(endpos(s1) \cap endpos(s2) = \varnothing\)。

    首先证明 \(s1\) 是 \(s2\) 的后缀 \(\Rightarrow\) \(endpos(s1) \supseteq endpos(s2)\)。

    既然 \(s1\) 是 \(s2\) 后缀,所以每次 \(s2\) 出现时 \(s1\) 也必然伴随出现,所以有 \(endpos(s1) \supseteq endpos(s2)\)。

    再证明 \(endpos(s1) \supseteq endpos(s2)\) \(\Rightarrow\) \(s1\) 是 \(s2\) 的后缀。

    我们知道对于 \(S\) 的子串 \(s2\),\(endpos(s2)\)不会是空集,所以 \(endpos(s1) \supseteq endpos(s2)\) \(\Rightarrow\) 存在结束位置 \(x\) 使得 \(s1\) 结束于 \(x\),并且 \(s2\) 也结束于 \(x\),又 \(|s1| \le |s2|\),所以 \(s1\) 是 \(s2\) 的后缀。

    综上可知,\(s1\) 是 \(s2\) 的后缀当且仅当 \(endpos(s1) \supseteq endpos(s2)\)。

    而 \(s1\) 不是 \(s2\) 的后缀当且仅当 \(endpos(s1) \cap endpos(s2) = \varnothing\)是一个简单的推论,不再赘述。

  2. 我们用 \(substrings(st)\) 表示状态 \(st\) 中包含的所有子串的集合,\(longest(st)\) 表示 \(st\) 包含的最长的子串,\(shortest(st)\) 表示 \(st\) 包含的最短的子串。

    例如对于状态 \(7\),\(substring(7)=\{aabbab,abbab,bbab,bab\}\),\(longest(7)=aabbab\),\(shortest(7)=bab\)。

  3. \(\text{SAM}\) 中的一个状态包含的子串都具有相同的 \(endpos\),它们都互为后缀。

    例如上图中状态 \(4\),\(\{bb,abb,aabb\}\)。

  4. 对于一个状态 \(st\),以及任意 \(s \in substrings(st)\),都有 \(s\) 是 \(longest(st)\)的后缀。

    因为 \(endpos(s)=endpos(longest(st))\),所以 \(endpos(s) \supseteq endpos(longest(st))\),根据刚才证明的结论有 \(s\) 是 \(longest(st)\) 的后缀。

  5. 对于一个状态 \(st\),以及任意的 \(longest(st)\) 的后缀 \(s\),如果 \(s\) 的长度满足:\(length(shortest(st)) \le length(s) \le length(longsest(st))\),那么 \(s \in substrings(st)\)。

    因为 \(length(shortest(st)) \le length(s) \le length(longsest(st))\)

    所以 \(endpos(shortest(st)) \supseteq endpos(s) \supseteq endpos(longest(st))\)

    又 \(endpos(shortest(st)) = endpos(longest(st))\)

    所以 \(endpos(shortest(st)) = endpos(s) = endpos(longest(st))\)

    所以 \(s \in substrings(st)\)。

也就是说,\(substrings(st)\) 包含的是 \(longest(st)\) 的一系列 连续 后缀。

前面我们讲到 \(substrings(st)\) 包含的是 \(longest(st)\) 的一系列 连续 后缀。这连续的后缀在某个地方会“断掉”。

比如状态 \(7\),包含的子串依次是 \(aabbab,abbab,bbab,bab\) 。按照连续的规律下一个子串应该是 \("ab"\),但是 \("ab"\) 没在状态 \(7\) 里。

这是为什么呢?

\(aabbab,abbab,bbab,bab\) 的 \(endpos\) 都是 \(\{6\}\),下一个 \("ab"\) 当然也在结束位置6出现过,但是 \("ab"\) 还在结束位置 \(3\) 出现过,所以 \("ab"\) 比 \(aabbab,abbab,bbab,bab\) 出现次数更多,于是就被分配到一个新的状态中。

所以,当 \(longest(st)\) 的某个后缀 \(s\) 在新的位置出现时,就会“断掉”,\(s\) 会属于新的状态。

于是我们可以发现一条状态序列:\(7 \rightarrow 8 \rightarrow 5 \rightarrow S\)。这个序列的意义是 \(longest(7)\) 即 \(aabbab\) 的后缀依次在状态 \(7,8,5,S\) 中。我们用 后缀链接 (\(\text{Suffix Link}\)) 这一串状态链接起来,这条 \(\text{link}\) 就是上图中的 绿色虚线。

\(\text{Suffix Links}\)后面会有妙用,我们暂且按下不表。

SAM 的转移函数 (Transition Function)

对于一个状态 \(st\),我们首先找到从它开始下一个遇到的字符可能是哪些。我们将 \(st\) 遇到的下一个字符集合记作 \(next(st)\),有 \(next(st) = \{S[i+1] | i \in endpos(st)\}\)。

例如 \(next(S)=\{S[1], S[2], S[3], S[4], S[5], S[6], S[7]\}=\{a, b, d\}\),\(next(8)=\{S[4], S[7]\}=\{b, d\}\)。

对于一个状态 \(st\) 来说和一个 \(next(st)\) 中的字符 \(c\),发现 \(substrings(st)\) 中的所有子串后面接上一个字符 \(c\) 之后,新的子串仍然都属于同一个状态。

例如状态 \(4\),\(next(4)=\{a\}\),\(aabb,abb,bb\) 后面接上字符 \(a\) 得到 \(aabba,abba,bba\),这些子串都属于状态\(6\)。

所以对于一个状态 \(st\) 和一个字符 \(c \in next(st)\),可以定义转移函数 \(trans(st, c) = x | longest(st) + c \in substrings(x)\)。

也就是说,在 \(longest(st)\)(因为无论哪个子串都会得到相同的结果)后面接上一个字符 \(c\) 得到一个新的子串 \(s\),找到包含 \(s\) 的状态 \(x\),那么 \(trans(st, c)\) 就等于 \(x\)。

算法构造

构造方法

使用 增量构造 的方法可以在 \(O(|S|)\) 的时间和空间复杂度中构造出 \(\text{SAM}\),也就是从初始状态开始,每次添加一个字符 \(S[1], S[2], \dots S[n]\),依次构造可以识别 \(S[1], S[1\dots 2], S[1\dots 3], ... S[1\dots N]=S\) 的 \(\text{SAM}\)。

首先,为了实现 \(O(|S|)\) 的构造,每个状态肯定不能保存太多数据,例如 \(substring(st)\) 肯定不能保存下来了。对于一个状态 \(st\),只保存以下数据:

数据 含义
\(maxlen[st]\) \(st\) 包含的最长子串的长度
\(minlen[st]\) \(st\) 包含的最短子串的长度
\(trans[st][1\dots c]\) \(st\) 的转移函数,\(c\) 为字符集大小
\(slink[st]\) \(st\) 的后缀链接 \(\text{(Suffix Link)}\)

假设已经构造好了 \(S[1\dots i]\) 的 \(\text{SAM}\),此时需要添加字符 \(S[i+1]\),于是新增了 \(i+1\) 个后缀需要识别:\(S[1\dots i+1],S[2\dots i+1],\dots,S[i+1]\)。由于这些新增状态分别是从\(S[1\dots i],S[2\dots i],\dots,""\) 通过字符 \(S[i+1]\) 转移过来的,所以我们还需要对这些状态添加相应的转移。

假设 \(S[1\dots i]\) 对应的状态是 \(u\),等价于 \(S[1\dots i]\in substrings(u)\)。根据前面的讨论我们知道 \(S[1\dots i], S[2\dots i], S[3\dots i], \dots , S[i], ""\) 对应的状态集合恰好就是从 \(u\) 到初始状态 \(S\) 的由 \(\text{Suffix Link}\) 连接起来路径上的所有状态,不妨称这条路径 (上所有状态集合) 是 \(\text{suffix-path}(u\rightarrow S)\)。

显然至少 \(S[1\dots i+1]\) 这个子串不能被以前的 \(\text{SAM}\) 识别,所以至少需要添加一个状态 \(z\),\(z\) 至少包含\(S[1\dots i+1]\)这个子串。

  1. 首先考虑最简单的一种情况:对于 \(\text{suffix-path}(u\rightarrow S)\) 的任意状态 \(v\),都有 \(trans[v][S[i+1]]=NULL\)。这时我们只要令 \(trans[v][S[i+1]]=z\),并且令 \(slink[st]=S\) 即可。

    例如已经得到 \("aa"\) 的 \(\text{SAM}\),现在希望构造 \("aab"\) 的 \(\text{SAM}\)。

    此时 \(u=2,z=3\),\(\text{suffix-path}(u\rightarrow S)\) 是桔色状态组成的路径 \(2-1-S\)。并且这 \(3\) 个状态都没有对应字符 \(b\) 的转移。所以我们只要添加红色转移 \(trans[2][b]=trans[1][b]=trans[S][b]=z\) 即可。以及 \(slink[3]=S\)。

  2. 另一种复杂一点的情况是 \(\text{suffix-path}(u\rightarrow S)\) 上有一个节点 \(v\),使得 \(trans[v][S[i+1]] \neq NULL\)。

    先以下图为例。假设已经构造了 \("aabb"\) 的 \(\text{SAM}\) 如图,现在我们要增加一个字符 \(a\) 构造 \("aabba"\) 的 \(\text{SAM}\)。

    此时 \(u=4,z=6\),\(\text{suffix-path}(u\rightarrow S)\) 是桔色状态组成的路径 \(4-5-S\)。对于状态 \(4\) 和状态 \(5\),由于它们都没有对应字符 \(a\) 的转移,所以我们只要添加红色转移 \(trans[4][a]=trans[5][a]=z=6\) 即可。但是 \(trans[S][a]=1\) 已经存在。

    不失一般性,我们可以认为在 \(\text{suffix-path}(u\rightarrow S)\) 遇到的第一个状态v满足 \(trans[v][S[i+1]]=x\)。这时我们需要讨论 \(x\) 包含的子串的情况。如果 \(x\) 中包含的最长子串就是v中包含的最长子串接上字符S[i+1],等价于maxlen(v)+1=maxlen(x),比如在上面的例子里,\(v=S, x=1\),\(longest(v)\) 是空串,\(longest(1)="a"\) 就是 \(longest(v)+'a'\)。这种情况比较简单,我们只要增加 \(slink[z]=x\) 即可。

    如果\(x\) 中包含的最长子串不是 \(v\) 中包含的最长子串接上字符 \(S[i+1]\),等价于 \(maxlen(v)+1 < maxlen(x)\),这种情况最为复杂,不失一般性,用下图表示这种情况,这时增加的字符是 \(c\),状态是 \(z\)。

    在 \(\text{suffix-path}(u\rightarrow S)\) 这条路径上,从u开始有一部分连续的状态满足 \(trans[u..][c]=NULL\),对于这部分状态我们只需增加 \(trans[u..][c]=z\)。紧接着有一部分连续的状态 \(v..w\) 满足 \(trans[v..w][c]=x\),并且 \(longest(v)+c\) 不等于 \(longest(x)\)。这时我们需要从 \(x\) 拆分出新的状态 \(y\),并且把原来 \(x\) 中长度小于等于 \(longest(v)+c\) 的子串分给 \(y\),其余字串留给 \(x\)。同时令 \(trans[v..w][c]=y\),\(slink[y]=slink[x], slink[x]=slink[z]=y\)。

    举个例子。假设我们已经构造 \("aab"\) 的 \(\text{SAM}\) 如图,现在我们要增加一个字符 \(b\) 构造 \("aabb"\) 的\(\text{SAM}\)。

    当我们处理在 \(\text{suffix-path}(u\rightarrow S)\) 上的状态 \(S\) 时,遇到 \(trans[S][b]=3\)。并且 \(longest(3)="aab"\),\(longest(S)+'b'="b"\),两者不相等。其实不相等意味增加了新字符后 \(endpos("aab")\) 已经不等于 \(endpos("b")\),势必这两个子串不能同属一个状态 \(3\)。这时我们就要从 \(3\) 中新拆分出一个状态 \(5\),把 \("b"\)及其后缀分给 \(5\),其余的子串留给 \(3\)。同时令 \(trans[S][b]=5, slink[5]=slink[3]=S, slink[3]=slink[4]=5\)。

    此处加入一些个人理解:对于一条 \(\text{suffix-path}(u\rightarrow S)\) 所包含的所有子串,其必然是连续的,也就是说,在路径上的同一个状态里内的子串,\(append\) 字符 \(S[i+1]\) 之后其 \(endpos\) 集合都还是相等的。然后考虑当某个状态已经有了字符 \(S[i+1]\) 的转移时,若对应 \(append\) 字符 \(S[i+1]\) 后所对应的子串其所在的状态的等价类最长的字符串,那么由于比它长的串都必定不在 \(\text{suffix-path}(u\rightarrow S)\) 上的同一个状态内,所以它们的 \(endpos\)$ 集合也必定已经不同。

参考资料

hihoCoder

posted @ 2020-01-05 22:38  newbielyx  阅读(420)  评论(0)    收藏  举报