回文自动机学习笔记

最近学了回文自动机这个神奇的东西,写篇文章加深下印象,如有不足或错误,欢迎指出。

墙裂建议自行画图加深理解。

回文串的定义点

问题引入

给定一个字符串 $s$,求以 $s$ 每个位置结尾的回文子串个数。

如果用暴力枚举每个串的方式,时间复杂度可能是 $O(n^2)$ 或者更高,主要是花在寻找以新位置结尾的回文串的重新枚举上。

回文串有个显然的性质,即去掉相同长度的前缀与后缀仍是回文串,利用这种包含关系,也许可以提高计算的效率。

回文自动机利用了这个性质,可以 $O(n)$ 处理求本质不同回文子串个数的这类回文串题目。

基本概念

回文自动机,即 Palindrome Automaton(PAM),又名回文树。

回文自动机由两颗树构成,分别有奇根和偶根,对应着长度为奇数和偶数的回文串,奇根编号是 $1$,偶根编号是 $0$。

在回文自动机中,包含了所有的回文串,每个节点表示一个回文串,其信息存到边上,维护 $last$ 表示以上次插入的字符结尾的最长回文串对应节点,$last$ 初始值为 $1$。对于点 $x$,维护 $len_x$,表示 $x$ 代表的回文串长度。用 $ch_{x,\Sigma}$ 表示 $x$ 每条出边对应的儿子,类似 Trie。

特别地,$len_0=0,len_1=-1$,即偶根长度为 $0$,奇根长度为 $-1$。奇根长度 $-1$ 的妙处后文会提到。

以回文串 $\texttt{abaabba}$ 为例,下图即为仅含回文树边的回文自动机。

某个点代表的回文串在图上的表示,是从奇根或偶根开始,经过一条转移边,即在回文串的前后各加一个相同的字符,这样对于回文串的表示显然是有正确性的。特别地,奇根所在树的节点回文中心的字符仅加一次。如路径 $1\rightarrow3\rightarrow4$ 表示的回文串是 $\texttt{aba}$,路径 $0\rightarrow5\rightarrow6$ 表示的回文串是 $\texttt{baab}$。

因为每次转移会使其表示的回文串前后各扩展一个相同的字符,使长度加 $2$,所以对于点 $x$,若其父节点为 $fa$,则 $len_x=len_{fa}+2$。

和其他自动机一样,回文自动机也有 $fail$ 指针,对于点 $x$,$fail_x$ 指向点 $x$ 代表的回文串最长的回文真后缀所对应节点,特别地,$fail_0=1,fail_1=1$。很多题解都说奇根的 $fail$ 指针没有意义,但在实现中指向自身要好处理。

还是回文串 $\texttt{abaabba}$,下图即其完整的回文自动机。

构造方法

对于字符串 $s$,若已构造好前 $i-1$ 个字符的回文自动机,加入字符 $s_i$,从 $last$ 表示的回文串开始,然后不断跳 $fail$ 边,直到找到某个节点 $x$,使 $s_{i-len_x-1}=s_i$,即该节点表示的回文串上一个字符与待添加字符相同时,$s_{i-len_x-1}$ 至 $s_i$ 所构成的回文串即为以 $i$ 结尾的最长回文串。

定义一个 $\text{getfail}$ 函数,$\text{getfail}(x,i)$ 返回的即为加入 $s_i$ 后满足上述条件的点 $x$。

int getfail(int x, int i) {
    while(i - len[x] - 1 < 0/*判断边界*/ || s[i - len[x] - 1] != s[i]) x = fail[x];
    return x;
}

这个正确性证明很显然,因为以 $i$ 结尾的长度不为 $1$ 的回文串,必定包含 $s_{i-1}$,又因为回文串去掉相同长度的前后缀仍是回文串,所以若以 $i$ 结尾的最长回文串长度不为 $1$,必定包含以 $i-1$ 结尾的回文串,可以通过 $fail$ 指针找到所有以 $i-1$ 结尾的回文串。

以 $i$ 结尾的回文串中必有长度为 $1$ 的回文串,即 $s_i$ 单独成串,此时不包含 $s_{i-1}$,需单独处理。前文提到 $len_1=-1$,所以 $s_{i-len_1-1}=s_i$,即 $fail$ 指针指向奇根时,奇根会让下一次匹配到自身,显然符合,又因为所有节点的 $fail$ 指针都直接或间接指向奇根,也就保证了必定能找到满足条件的点 $x$,且 $i-1$ 结尾的回文串均可通过 $fail$ 指针找到,$\text{getfail}$ 的正确性得证。

若 $x$ 现在有一条表示 $s_i$ 的出边,即已存在与 $i$ 结尾的最长回文串本质相同的节点,直接更新 $last$ 就好。若没有,则新建一个,再求它的 $fail$ 指针。还是先找到 $last$ 表示的回文串中满足两边扩展 $s_i$ 仍是回文串的回文后缀,发现和上文的求法是类似的,可以用 $\text{getfail}(fail_x,i)$ 来求出其满足条件的回文后缀所对应的节点。

插入 $s_i$ 的代码如下:

void insert(char c, int i) {
    int x = getfail(last, i), w = c - 'a';
    if(!ch[x][w]) {
        len[++cnt] = len[x] + 2;
        int tmp = getfail(fail[x], i);
        fail[cnt] = ch[tmp][w];
        //things to do
        ch[x][w] = cnt;
    }
    last = ch[x][w];
}

注意设定父子关系是最后处理,原因是当 $ch_{tmp,w}$ 不存在时,即新建的节点的 $fail$ 未匹配到,因 $ch_{twp,w}$ 初值为 $0$,$fail_{cnt}$ 会被设为 $0$,即新建的节点 $fail$ 指针指向偶根,$len_0=0$,空串显然可行。

复杂度证明

证明过程和 KMP 很相似。

可以证明,对于一个字符串 $s$,它的本质不同回文子串个数最多只有 $|s|$个(然而我不会,网上应该都有)。

因为回文自动机上每个节点代表的回文串均不同且包含了 $s$ 所有的回文串,所以最多有 $|s|$ 个节点,令 $n=|s|$。

若不计 $\text{getfail}$ 复杂度,其它操作复杂度 $O(n)$ 比较显然。

因为 $fail$ 指向的是回文真后缀,所以长度必更小,跳 $fail$ 边深度单调非递增,且同一深度最多跳两次 $fail$ 边(奇根所在树与偶根所在树),因每次新建节点 $fail$ 指针指向节点深度只会加 $1$,所以最多可跳 $2n$ 次 $fail$ 边。

总时间复杂度 $O(n)$,空间复杂度为 $O(n\Sigma)$。

例题

模板题为例。

用 $sum_x$ 表示答案,显然以 $i$ 结尾的回文串个数即以 $i$ 结尾的最长回文串的最长回文真后缀的回文串个数,即 $sum_x=sum_{fail_x}+1$。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10, Sigma = 26;
char s[N];
int lastans, n;
struct Palindrome_Automaton {
    int ch[N][Sigma], fail[N], len[N], sum[N], cnt, last;
    Palindrome_Automaton() {
        cnt = 1;
        fail[0] = 1, fail[1] = 1, len[1] = -1;
    }
    int getfail(int x, int i) {
        while(i - len[x] - 1 < 0 || s[i - len[x] - 1] != s[i]) x = fail[x];
        return x;
    }
    void insert(char c, int i) {
        int x = getfail(last, i), w = c - 'a';
        if(!ch[x][w]) {
            len[++cnt] = len[x] + 2;
            int tmp = getfail(fail[x], i);
            fail[cnt] = ch[tmp][w];
            sum[cnt] = sum[fail[cnt]] + 1;
            ch[x][w] = cnt;
        }
        last = ch[x][w];
    }

} PAM;
int main() {
    scanf("%s", s + 1);
    int len = strlen(s + 1);
    for(n = 1; n <= len; n++) {
        s[n] = (s[n] - 97 + lastans) % 26 + 97;
        PAM.insert(s[n], n);
        printf("%d ", lastans = PAM.sum[PAM.last]);
    }
    return 0;
}

还有一些简单题:

P4555 [国家集训队]最长双回文串

P4287 [SHOI2011]双倍回文

P3649 [APIO2014]回文串

posted @ 2021-07-04 11:50  Terac  阅读(32)  评论(0)    收藏  举报  来源