$\text{Suffix Automaton}$ 学习笔记

学这个学了好几次了,这是学的最认真也最深的一次,至少看懂概念了。

感谢 XJ 的断网。


\(\text{Suffix Automaton}\) 是一个接受一个字符串所有后缀信息的自动机,且其结点/边最少。(线性)

实质上,由于它接受的是所有后缀,所以从根出发的任意一条路径都代表某个后缀的前缀,也就是一个子串。

所以实质上这家伙存下了所有子串信息。


\(\text{endpos,len}\):

把一个子串在原串中所有出现位置的结束位置整成一个集合,叫它 \(\text{endpos}\) ,用 \(\text{endpos(x)}\) 表示子串 \(x\)\(\text{endpos}\)

我们按 \(\text{endpos}\) 把子串们分成许多个等价类。一个 \(\text{endpos}\) 与一个等价类相互对应。

那么我们可以得到一个显然的引理:子串 \(\alpha\) 是子串 \(\beta\) 的一个后缀等价于 \(\text{endpos}(\alpha)⊇\text{endpos}(\beta)\)

然后还可以得到一些定理:

\(\text{endpos}\) 定理 \(1\)\(\forall\) 子串 \(\alpha,\beta,|\alpha| \ge |\beta|\) ,有 \(\text{endpos}(\alpha)∩\text{endpos}(\beta) = \phi\) 或 $\text{endpos}(\beta) ⊇ \text{endpos}(\alpha) $

证明:考虑若 \(\text{endpos}(\alpha) ∩ \text{endpos}(\beta) \ne \phi\) ,那么显然有 \(\beta\)\(\alpha\) 的后缀,根据引理得证。

\(\text{endpos}\) 定理 \(\text{2}\)\(\forall\) 等价类 \(x\) ,令其中最短串长度为 \(l\) ,最长串长度为 \(r\) ,则这个等价类内的字符串长度不重不漏的覆盖 \([l,r]\)

证明:令 \(x\) 内最短串为 \(\text{Min}\) ,最长串为 \(\text{Max}\)

不重:根据引理我们知道该等价类内的字符串一定是 \(\text{Max}\) 的后缀,而一个字符串不会有两个长度相等的后缀;

不漏: \(\forall t \in [l,r]\) ,考虑 \(\text{Max}\) 长度为 \(t\) 的后缀 \(w\) ,根据引理可以得到 \(\text{endpos}(\text{Min}) ⊇ \text{endpos}(w) ⊇ \text{endpos}(\text{Max})\) ,由于 \(\text{endpos}(\text{Max}) = \text{endpos}(\text{Min})\) ,可以得到三者相等。

\(\text{len}\) :记 \(\text{len}(x)\) 表示等价类 \(x\) 内最长串的长度。


\(\text{link,parent tree}\) :

对于不含空串的等价类 \(x\) ,若其中最短串长度为 \(t\) ,则其中任意一个字符串长度为 \(t-1\) 的后缀一定属于另一个等价类 \(y\)

\(\text{link}(x) = y\) 。实质上 \(\text{link}\) 是原串的一个子串的后缀们所形成的等价类间的链接。

\(\text{link}\) 定理 \(1\) :若将每个等价类视为点,且对于每个等价类 \(x\) ,从 \(x\)\(\text{link}(x)\) 连边,则等价类将形成一棵树,其根所代表的等价类中有且只有空串。

证明:首先,\(\text{len}(x) > \text{len(link(x))}\) ,因此不会形成环;其次,除只含空串的等价类外,每个等价类都有且只有一条出边。

将形成的这棵树称为 \(\text{parent tree}\)

\(\text{link}\) 定理 \(2\) :在 \(\text{parent tree}\) 上,对于任意一个等价类 \(x\) ,由根到 \(x\) 的路径上每个点所代表的等价类中所包含的子串长度不重不漏的覆盖 \([0,\text{len(x)}]\)

证明:好像可以根据 \(\text{link}\) 定义和 \(\text{endpos}\) 定理 \(2\) 直接推出?

\(\text{Suffix Automaton}\) 会同时维护 \(\text{link}\)

重要性质:\(\text{parent tree}\) 上一个结点的 \(\text{endpos}\) 可以通过合并其儿子的 \(\text{endpos}\) ,再加上这个结点的最长串作为原串的一个前缀时出现的位置得到。(当然,这个结点的最长串不一定以前缀的形式出现过)

证明:令当前结点为 \(x\) ,其最长串为 \(s\) 。考虑 \(s\) 除了第一次作为前缀出现时外的情况,\(s\) 前面一定有一个字符。根据 \(\text{link}\) 定义,加上这个字符后,\(s\) 所属等价类一定是 \(\text{parent tree}\)\(x\) 的儿子,那么这个出现位置可以通过合并儿子得到。

\(s\) 第一次作为整个串的前缀出现时的位置是特殊的。


\(\text{Suffix Automaton}\) 的点数/边数都是 \(O(n)\) 的。


出边

一条从 \(u\) 出发,值为 \(c\) 的边连向将 \(u\) 内所有串末尾加上 \(c\) 所得到的串们所属 \(\text{endpos}\) 的对应等价类。

如在串 \(cacaa\) 中,等价类 \(\{a\} \to_c = \{ac,cac\}\)


构建:

考虑在原串末尾新增一个字符 \(c\) 会产生的变化:

1:所形成的新串的 \(\text{endpos}\) 有且只有新串末尾,出现新的等价类,即新的点;

2:出现新的后缀,需要加入自动机;

3:原有子串的 \(\text{endpos}\) 集合发生变化,导致原有的等价类发生变化,需要修改;

首先,新开一个结点 \(cur\) 表示 \(\text{endpos}\) 有且只有新串末尾的等价类。如果新增字符前 \(\text{endpos}\) 有且只有末尾的等价类为 \(las\) ,则 \(\text{len}(cur) = \text{len}(las)+1\)

这个点的 \(\text{link}\) 先不管。

然后考虑加入新的后缀:这显然只与原串的后缀有关。从 \(las\) 开始一路跳 \(\text{link}\) 就可以找到这些等价类。

然后,若当前等价类为 \(p\) ,若 \(p\) 没有 \(c\) 的出边,那么给它来一条 \(c\) 的出边,指向 \(cur\)

否则,说明 \(p\) 中任意一个子串后面加上 \(c\) 所形成的串,在原串中已经出现过了;并且,再往后跳 \(\text{link}\) 所得到的所有后缀加上 \(c\) 所形成的串也已经出现过了。那么可以不再跳了。

然后尝试连 \(cur\)\(\text{link}\)

如果跳 \(p\) 的时候非常欢快,一路上都是没有 \(c\) 的出边,这个 \(p\) 直接跳到根去了,那么所有新的后缀都是只出现在末尾的,直接把 \(\text{link}(cur)\) 标成根。

否则,我们让 \(p\) 停在第一个有 \(c\) 的出边的位置。令 \(p\)\(c\) 出边指向 \(q\)

如果 \(\text{len}(p)+1=\text{len}(q)\) ,说明 \(q\) 内最长串是由 \(p\) 内最长串加上 \(c\) 形成的。由于 \(p\) 里面都是原串的后缀,\(q\) 里面就都是新串的后缀。并且,由跳 \(p\) 的方法可以知道,新串的后缀中,长度在 \(\text{len}(q)\) 以上的都不曾出现过,而长度小于等于 \(\text{len}\) 的都可以从 \(q\) 开始跳 \(\text{link}\) 得到。那么将 \(\text{link}(cur)\) 标成 \(q\)

否则,说明 \(q\) 内最长串是由一段东西加上 \(p\) 内最长串加上 \(c\) 形成的。

假设这个 \(q\) 内最长串是新串的一个后缀,那么这家伙去掉末尾的 \(c\) 就是原串的一个后缀,且其长度大于 \(\text{len}(p)\) ;但是我们跳到这个原串后缀的时候认为其加上 \(c\) 所得的串在原串中不存在,因此才跳到了 \(p\) 。那么与假设矛盾,这个最长串必然不是新串后缀;但是 \(p\) 内最长串加上 \(c\) 所得结果是新串的一个后缀,且二者同处于一个等价类中,这不符合定义,需要修改。

这个时候这个 \(q\) 当场裂开,把其中长度 \(\le \text{len}(p)+1\) 的拿出来放到一个新的等价类 \(q'\) 里面。其 \(\text{len}\) 显然为 \(\text{len}(p)+1\)

根据 \(\text{link}\) 定义,\(\text{link}(q') = \text{link}(q),\text{link}(q) = q'\)

这个 \(q'\)\(q\) 相比唯一多出的一个结束位置是在末尾。那么,给其中串末尾安上一个字符后得到的串集所对应 \(\text{endpos}\) 不会受到这个新增结束位置的影响。

于是可以直接把 \(q\) 的出边继承给 \(q'\)

同时,需要修改 \(q'\) 的入边:对于 \(\text{len}\le\text{len}(p)\) 且其出边 \(c\) 连向 \(q\) 的等价类,其出边 \(c\) 应连向 \(q'\)

\(p\)\(\text{link}\) 就能找到这些等价类;若当前等价类出边 \(c\) 不连向 \(q\) ,其必连向一个 \(\text{endpos}\) 包含 \(\text{endpos}(q)\) 的等价类;而之后再跳所得等价类的出边所连的 \(\text{|endpos|}\) 越来越大,不可能回到 \(q\) ,此时可以停止跳 \(\text{link}\)

最后,与 \(\text{len}(p)+1=\text{len}(q)\) 时的分析一样,将 \(\text{link}(cur)\) 标为 \(q'\)

再没有需要修改的了。

直接按上面说的模拟。复杂度是 \(O(n \Sigma)\) 的,那个 \(\Sigma\) 表示字符集大小。


应用

拓扑序:常规方法得到的拓扑序不一定同时适用于 \(\text{parent tree}\)\(\text{Suffix Automaton}\)

但是注意到 \(\text{len}(x)>\text{len}(\text{link(x)})\) ,并且对于任意一条出边 \(u \to v\)\(\text{len}(v)>\text{len(u)}\)

所以按照 \(\text{len}\) 将所有结点排序所得结果同时适用于 \(\text{parent tree}\)\(\text{Suffix Automaton}\)

值域由 \(\text{len}\) 决定,\(\text{len}\) 与字符串长度同阶,因此这个排序可以线性。


本质不同子串个数:

一种子串显然属于且只属于一个等价类,那么本质不同子串个数就是所有等价类所含的子串个数和。

\[\sum \text{len}(x)-\text{len}(\text{link}(x)) \]


子串出现次数:也就是 \(\text{endpos}\) 集合大小。

注意到 \(\text{parent tree}\) 的重要性质。其中,最长串作为前缀出现的等价类就是在构建自动机时新增的 \(cur\) 。因此,只需要把 \(cur\) 的出现次数标为 \(1\) ,构建完成后在 \(\text{parent tree}\) 上自下往上更新就完成了。


维护 \(\text{endpos}\) :有些题目需要这个。

利用 \(\text{parent tree}\) 的重要性质,与统计子串出现次数类似,用支持合并的数据结构大力维护一下。


求两个串的 \(\text{lcs}\) 长度:

先在第一个串上建出后缀自动机,然后把下一个串在上面跑。能走出边就走出边并将匹配长度 \(+1\),否则说明需要缩短匹配的前缀,直接跳 \(\text{link}\) 即可,每跳一次 \(\text{link}\) 就将匹配长度置为当前位置的 \(\text{len}\) ,直到跳到有出边的位置或跳到根。

复杂度听起来很假,但是注意到在第二个串上左匹配位置递增,右匹配位置也递增,所以是线性的。

扩展到多个串:记录 \(\text{Max}(x)\) 表示匹配当前串时,在后缀自动机上匹配到 \(x\) 这个位置的最大匹配长度。

成功走出边,然后将匹配长度 \(+1\) ,记录在当前位置。

注意到 \(\text{Max(x)}\) 所表示的方案对 \(\text{Max}(\text{link}(x))\) 同样适用,于是需要在 \(\text{parent tree}\) 上自下往上更新。但是要注意需要将 \(\text{Max}\)\(\text{len}\)\(\min\)

然后记录一个 \(\text{Min}\) 数组记录 \(\text{Max}\) 的历史最小值,最后 \(\text{Min}\) 里的最大值就是答案。


posted @ 2021-03-26 12:04  RUI_R  阅读(158)  评论(0编辑  收藏  举报