以前学的后缀自动机全都忘光了...这篇文章是笔者复习时写的,记录了一些比较要点的概念与构建步骤和部分原理及性质。还可以帮你快速记忆后缀自动机的步骤。
如果觉得本文章过于刻板的话,在hihocoder的problem set中搜索“后缀自动机”,题目中有后缀自动机详解。那里有举例说明,可能更加生动形象。
简介
后缀自动机是一个有限状态自动机。可以接受所有原字符串的后缀。即从自动机的起点到任何一个终点都是后缀自动机的一个后缀。
比如下面这张图是字符串abbb
的后缀自动机:
\(t_0\) 是出发点,灰色节点是中间点,绿色结点是终点。
既然它能够包含原字符串的所有后缀,那么从起点到任何一个结点(不一定是终止接点)都是原字符串的子串。
基本概念及性质
endpos 集合:也称 rightpos 集合。对于一个字符串 \(s\) 的子串 \(t\) ,\(t\) 的 endpos 集合是 \(t\) 的每个出现的位置的终点集合。
SAM中的一个结点代表一个等价类,即对于任何两个属于统一等价类的子串 \(s\) 和 \(t\) 都满足匹配到的最终结点相同。
一个节点的 len 属性:\(\text{len}_i\) 表示 \(i\) 结点所代表的等价类中最长字符串的长度。
parent 树:对于一个结点 \(i\) ,它的 endpos 集合被结点 \(j\) 包含,并且不存在结点 \(k\) 包含结点 \(i\) 的 endpos 集合且结点 \(k\) 的 endpos 集合被结点 \(j\) 的 endpos 集合包含,那么 \(j\) 就是 \(i\) 的父亲。这样的包含关系连成一棵树。这棵树叫做 parent 树。一个结点 \(i\) 的父亲 fa[i]
所在节点在
构建
采用增量算法构建。
每次考虑添加一个字符并随时维护后缀自动机。添加一个字符就意味着添加一个结束位置,所以必然添加一个结点,并且所有在只有最后出现过的字符串所在的结点有可能需要向新添加的字符连边,边上的字符为新添加字符。
现在的问题就是找出所有在最后出现过的字符串。设目前字符串 s 的长度为 n (添加完最后一个字符后长度为 n),那么在最后的字符串为 s[i : n] ,也就是 s[i : n-1]+s[n](加法表示拼接) 。所以只要把所有代表 s[i : n-1] 的字符串都找到,然后看情况更改就行。
那么如何找到代表 s[i: n-1] 的结点呢?考虑到 s[i : n-1] 即所有 endpos 集合包含 n-1 这个位置的字符串。所以可以从 s[1 : n-1] 的后缀自动机的最后一个字符向上跳来找到所有这样的字符串。
那么现在可以找到所有需要修改的结点。现在的问题就是根据不同的情况对结点加边。
-
对于当前节点,如果它没有一条字符为 s[n] 的转移边,那么就给他加上一个字符为 s[n] ,到新加结点的转一边。不向上跳父亲,直到当前点有一条字符为 s[n] 的转移边 或 跳到了根节点的父亲(也就是 0)。
-
若跳到了 0 号点,那么就没事了。
-
否则跳到了一个有 s[n] 转移边的结点,设这个结点为 p ,p 从 s[n] 转移到 q。那么 q 结点所代表的字符串中长度为 p+1 的字符串就多了一个 endpos 。下面分为两种情况:
-
若 \(\text{len}_q=\text{len}_p+1\) ,也就是说结点 q 只能代表一个字符串。此时并不需要做什么特别的。
-
若 \(\text{len}_q\not = \text{len}_p+1\) ,那么 \(q\) 代表了许多字符串。而长度 \(\le \text{len}_p+1\) 的字符串的 endpos 集合包含 n ,而长度 \(> \text{len}_p+1\) 的字符串的 endpos 不包含 n 的,所以需要将这些字符串分为两部分,这样才能保证一个结点代表一个 endpos 。
那么就将 q 结点分裂出结点 nq 。将 p 的字符为 s[n] 的出边重定向为 nq 。而所有长度 \(\le \text{len}_p+1\) 的字符串的 endpos 集合中均有 n 这个位置,需要将每一个长度 \(\le \text{len}_p+1\) 的结点都重定向。
容易发现有 s[n] 这条边的结点必然是在 parent 树上是连续的。这是因为 parent 树向上 endpos 集合大小会不断增加,所以如果下面没有,上面就更不可能有。所以向上连续跳着修改就行了。
-
-
上面并没有说明
len
和par
更改的细节。
代码
注意每次操作一个结点的最长字符串、parent 树上的父亲的变化。具体代码就不贴了吧。