正向思维理解后缀自动机
大部分网上对SAM 的讲解逻辑很混乱,让我对 SAM 只能知其然,而不知其所以然,所以在这里理清了 SAM 的逻辑链
前置知识 : DFA
建议对 SAM 有个基本概念再来看此文
注意:有什么没写明的符号,在参考文献1里面已经说明
后缀自动机的定义
后缀自动机(SAM)就是接受一个字符串所有后缀的最小DFA。
但是这个定义过于迷惑,我们考虑换一种等价定义:
SAM是以 \(endpos\) 等价类为状态的,接受所有后缀的DFA。
(\(endpos\) 将在下文说明)
证明定义的等价性:
- 可以构造出一个以 \(endpos\) 等价类为状态的,接受所有后缀的DFA
- 这个DFA是最小的 (此处证明较为复杂,略去)
这个定义修改是关键的,之后的一切操作、性质都以新定义位出发点
后缀自动机的性质
- 是证明边 \(O(n)\) 的前置知识
建议理解 \(endpos\) 等价类后再来理解这里
在下文讲解中,我们默认"路径”是指从源点(起始状态)出发的路径
- 性质1
每条终点为接受状态的路径均代表一个后缀 - 性质2
每条路径均代表一个子串 - 性质3
不同路径代表的子串是本质不同的
后缀自动机的理论基础
\(endpos\) 及其等价类的性质
\(endpos(str)\) 是一个从字符串子串到一个集合的映射
它表明一个子串在字符串中每次出现的结束位置
\(endpos\) 等价类是所有 \(endpos\) 相同的子串
具体地,对aabab这个字符串的子串ab来说
\(endpos("ab") = \{3, 5\}\)
(在本文中,我们默认字符串首字母的下标是1)
下面的性质讲不给出证明,请见互联网
- 性质1
如果两个子串的 \(endpos\) 相同,一个字符串是另一个的后缀 - 性质2
两个子串的 \(endpos\) 只有相交或包含关系 - 性质3
一个 \(endpos\) 等价类里面所有子串长度连续 - 性质4
\(endpos\) 等价类的个数 \(O(n)\) - 性质5
\(endpos\) 等价类中的所有子串同时在末尾添加一个字母得到的子串,均为同一个等价类
其中,
性质1、2为性质3提供证明
性质2 暗示我们,所有 \(endpos\) 等价类可以构成一棵树, 我们把这棵树叫做 parent_tree
性质4 证明了 SAM 的状态数为 \(O(n)\)
性质5 解释了 SAM 的转移,告诉我们 SAM 是存在的
后缀自动机的状态数 \(O(n)\)
性质4 上文已经给出证明
后缀自动机的转移数 \(O(n)\)
我们考虑 SAM 的一颗生成树
从每一个 SAM 的接受状态(代表了至少一个后缀)出发,按照后缀转移路径的相反顺序,去向源点反向转移,在反向转移中我们需要添加一些非树边,我们只要证明非树边的条数 \(O(n)\) 即可
其它子串是后缀的前缀,只要后缀满足路径可行,其它子串也满足路径可行
为了证明这个结论,我们可以证明对每一个后缀,我们只需要均摊添加不超过一个边即可
换句话说,我们可以证明每连一条边,必然能够反向转移一个新的后缀
需要连接 \(\leq 1\)条边的后缀是显然的,我们只考虑这样一个后缀,它需要连接 $ >1 $条边,我们先任意的连其路径上的一条边,然后不管它,去沿着到源点的路径反向转移
在连接一条新边后,因为这是一颗生成树,所以其到源点的路径一定是联通的,并且这条路径包含一个新边,所以其一定代表这一个新的后缀,我们证明了每连一条边,必然能够反向转移一个新的后缀
后缀自动机的构造
因为每个字符串前缀的 \(endpos\) 必然不相等,而且能从较短前缀转移到较长前缀,所以我们可以把 SAM 中代表前缀的若干个状态提出来,叫做前缀链
这样就把 SAM 分成两部分,前缀链和其它
我们每次在字符串结尾添加一个字符,维护 SAM ,最终即可做到 SAM 的构造
我们在添加一个字符之后,有一些字符串的 \(endpos\) 改变了,即状态的含义改变了,还有一些字符串的转移的 \(endpos\)改变了,即转移后的新状态的含义改变了, 我们必须重新进行维护 parent_tree(即fa数组) 和 SAM
设新字符为 \(c\) ,新串长度为 \(n\)
哪些字符串的 \(endpos\) 改变了?
只有原来 后缀+ "\(c\)" 字符串的 \(endpos\)改变了, 即比原先多了一个 \(n\)
现在就有一个很自然地(不完善的)思路,
我们从沿着 \(last\)向上跳到空状态,不难发现整条链都是原串后缀,我们将这条链称之为后缀链
我们就向上遍历后缀链,然后考察这个状态是否有 \(c\) 的转移,如果没有 \(c\)的转移,这就说明这个 该状态的字符串 + \(c\) 在旧串中是不存在的,不过现在随着新结点的加入而存在,我们可以直接连到新结点上
如果有\(c\)的转移,就说明这个字符串 +\(c\)在旧串中是存在的,那么它的后缀一定也有\(c\)的转移。
从而,我们现在就可以将新结点的\(fa\)设成第一个有\(c\)转移的状态的的\(c\)转移,这表明以新结点为叶子的链构成了一个新的后缀链。换句话说,新结点的\(fa\)是旧串中为新串后缀的最长子串。
我们到这里便找到了新结点的\(fa\)
但是,这个思路是错误的,在“从而”后面,我们少讨论了一种情况,旧串后缀 + \(c\)作为其它串的后缀了,且它们\(endpos\)相同,这个时候只有旧串后缀 + \(c\)的 \(endpos\)包含 \(n\), 而其它串不含\(n\),我们不得不把这个状态分裂,并且重新调整\(fa\)和转移
具体的\(aaba\) + \(b\)
"\(a\)"是旧串一个后缀,"\(a\)"+"\(b\)",是新串的一个后缀,\(endpos("aab") = \{ 3\}\) \(endpos("ab") =\{3, 5\}\)
所以此时\("aab"\)与\("ab"\)在一个状态里,我们只能将其分裂来保证 SAM的正确性
详细构造请见互联网
参考文献
https://www.luogu.com.cn/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie
https://oi-wiki.org//string/sam/#link