定义
PAM (Palindrome Automaton) 是一种处理回文串的、针对某个串的自动机,它保存了该串所有回文串的信息。
PAM 需要维护三个基础的东西:点、转移边、fail 边。PAM 和 AC 自动机类似,所以我们可以借鉴 AC 自动机来理解这些东西。
AC 自动机 | PAM | |
---|---|---|
点 | 一个前缀 | 一个回文串 |
转移边 | 从起点走到终点,相当于在起点的字符串后面加上一个字符 | 从起点走到终点,相当于在起点的字符串两侧都加上一个字符 |
fail 边 | 存在的最长后缀 | 存在的最长回文后缀 |
但是,PAM 有一个 AC 自动机不具有的性质——它有两个根!因为可以发现走转移边并不会改变回文串长度的奇偶性,这样的话就保存不了所有的回文串了,所以我们需要两个根,一个叫做奇根,还有一个叫做偶根,这样才能维护所有的回文串。
构造
定义完了那就看看怎么构造它。这里我们使用增量法。
为了方便,我们给每个点定义一个结构体,存当前点的信息,这里我们就维护三个信息:当前回文串的长度、转移边、fail 边。
1 | struct Node { |
先看看怎么初始化,显然我们要先定义两个根。对于奇根,它的长度为 -1
(可以把在它两侧加一个字符,当做它会吃掉一个字符),它一开始没有儿子,它的 fail 边随便指(因为它的两侧一定可以加字符)。对于偶根,它的长度为 0
(可以看成是空串),它一开始没有儿子,它的 fail 边指向奇根,因为它两侧不能加字符的话,就只能丢给奇根了。即:(0
号节点是偶根,1
号节点是奇根)
1 | t[0].len = 0; t[1].len = -1; |
假设上一次加入的点是 last
,现在需要新增一个字符 ch
,假设它存储在 s[pos]
。
首先我们需要找到一个 last
所代表串的一个最长后缀,使得可以在这个后缀后面加上 ch
,即
1 | int Getfail(int last, int pos) { |
我们把这个最长后缀所在的点记作 cur
,然后接下来就看 cur
节点有没有 ch
这个儿子,如果有就直接跳过,把 last
设为这个儿子,否则就新增一个节点,记为 nxt
。
对于 nxt
,它的长度是 cur
的长度 +2
(根据转移边的定义)。它的 fail 的寻找和 AC 自动机很像,是在 cur
的 fail
中再找到一个最长的有 ch
这个字符的儿子的后缀,然后把 nxt
的 fail 指向那个后缀的 ch
儿子。最后再把 cur
的 ch
儿子设为 nxt
。具体实现如下。
1 | int nxt = ++cnt; |
最后也需要让 last
等于 nxt
。
这样就可以把 PAM 构造出来,复杂度是 \(O(n)\) 的,因为每次跳 fail 会让当前串的长度至少减 2
,而加入一个字符只会让当前串的长度加 2
,所以跳 fail 的次数是有保障的。
应用
PAM 可以处理很多有关回文串的信息,具体在此就不展开了。实际上我们可以把 AC 自动机或是 SAM 的那些套路拿到 PAM 上来用,具体的怎么做还是得要进行实践。