回文自动机例题
今天学习了一下回文自动机,吊打\(manacher\)有没有(除了空间)
回文自动机基于这两个性质:
1.一个长度为\(n\)的字符串的本质不同回文子串是\(O(n)\)级别的
2.在一个字符串后增加一个字符后,最多新增\(1\)个本质不同回文子串
这两条性质都可以用归纳法证明
于是我们想到用一个结点来代表一个本质不同的回文串,然后就有\(PAM\)了。和\(AC\)自动机一样,\(PAM\)也有\(fail\)失配边和\(ch\)转移边,\(fail\)代表的是最长回文后缀
采用增量构造法来构建\(PAM\),在当前字符串末尾插入一个字符\(c\),如果出现了一个新的回文子串,就新建一个结点,并计算其失配边
另外最开始有两个结点,分别代表奇数长度的根结点和偶数长度的根结点
代码如下(细节看注释):
#define N 100000
int last, nid;
int ch[N+5][26], fail[N+5], len[N+5], cnt[N+5];
void init() {
fail[0] = fail[1] = nid = 1; //偶数的根结点为0,fail指向1;奇数为1,fail指向自己
len[1] = -1; //因为是奇数,所以不是0
}
void build(char *s) {
int n = strlen(s+1);
for(int i = 1; i <= n; ++i) {
int p = last, c = s[i]-'a';
while(s[i-len[p]-1] != s[i]) p = fail[p]; //直到能够形成回文串
if(!ch[p][c]) { //新的本质不同回文串
int v = ++nid, t = fail[p]; //把t设为fail[p]是因为后缀不能是它本身
len[v] = len[p]+2;
while(s[i-len[t]-1] != s[i]) t = fail[t]; //跳fail找最长回文后缀
fail[v] = ch[t][c]; //跟AC自动机有点像
ch[p][c] = v;
}
last = ch[p][c]; //更新last
cnt[last]++;
}
}
然后[APIO2014]回文串就变成板子了:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define N 300000
int last, nid;
int ch[N+5][26], fail[N+5], len[N+5], cnt[N+5];
long long ans = 0;
void init() {
fail[0] = fail[1] = nid = 1;
len[1] = -1;
}
void build(char *s) {
int n = strlen(s+1);
for(int i = 1; i <= n; ++i) {
int p = last, c = s[i]-'a';
while(s[i-len[p]-1] != s[i]) p = fail[p];
if(!ch[p][c]) {
int v = ++nid, t = fail[p];
len[v] = len[p]+2;
while(s[i-len[t]-1] != s[i]) t = fail[t];
fail[v] = ch[t][c];
ch[p][c] = v;
}
last = ch[p][c];
cnt[last]++;
}
}
void topo() {
queue<int> q;
int in[N+5] = {0};
for(int i = 0; i <= nid; ++i) in[fail[i]]++;
for(int i = 0; i <= nid; ++i) if(!in[i]) q.push(i);
while(!q.empty()) {
int u = q.front(); q.pop();
ans = max(ans, 1LL*len[u]*cnt[u]);
in[fail[u]]--, cnt[fail[u]] += cnt[u];
if(!in[fail[u]]) q.push(fail[u]);
}
}
int main() {
init();
char s[N+5] = {0};
scanf("%s", s+1);
build(s);
topo();
printf("%lld\n", ans);
return 0;
}
还有一道最长双回文串