【学习笔记】回文自动机初步总结
什么是PAM啊(战术后仰)
某日看到同学写了道回文自动机题的题解,于是一时兴起学了回文自动机并打了两道题。结果过了几周就忘了,甚至网课上老师提到PAM我以为是后缀自动机。现在想来甚是珂怕,故作此文。
回文自动机(PAM)是一种用于存储字符串回文信息的数据结构。
回文串分为奇回文串和偶回文串,因此我们开两棵树来存储它们,并在后继的描述中将其称作奇树和偶树。
回文自动机的构建
每个回文自动机节点存储着一个值 $len_i$ 表示这个点所代表回文串的长度,设当前节点为 $now$ ,那么 $tr_{now,i}$ 表示在 $now$ 节点所代表的回文串的前后各加一个字符 $i$ 所构成的回文串。显然,从根到 $now$ 的链保存了 $now$ 所代表的回文串的信息。
$fail : fail$ 指针存储着该节点所对应的 最长回文后缀 所对应的节点(显然最长回文后缀也一定在这棵树上)。
初始状态下,我们有两个节点,奇树和偶树的根,它们的 $len$ 分别是 $-1,0$ 。其中偶数的根的 $fail$ 指向奇数的根。
假设我们已经构建完了 $1 \sim i-1$ 的回文自动机,考虑加入第 $i$ 个字符。
-
找到 $1 \sim i-1$ 的 最长后缀回文串 对应的节点 $lst$ (在没有加入字符时 $lst$ 默认为偶树的根)。
-
尝试在 $lst$ 的前后加上字符 $i$ ,如果 $tr_{lst,i}$ 已经存在,则将 $tr_{lst,i}$ 设置为 $lst$ ,准备匹配第 $i+1$ 个字符。
-
如果 $tr_{lst,i}$ 为空,那我们需要构建该节点与其 $fail$ 指针。如果 $lst$ 所对应回文串的左端点的前一个字符与 $i$ 相同,那么新建一个节点 $tr_{lst,i}$ 表示以 $i$ 为右端点的最长回文串。否则找到 $lst$ 节点的 $fail$ (其同样是 $1 \sim i-1$ 的 后缀回文串),并重复上述操作。直到找到一个后缀回文串能在前后同时加上 $i$ ,或者找到偶树或奇树的根(代表此时没有已有回文串匹配)。设此时找到的节点为 $p$ ,新建一个节点 $tr_{p,i}$ 表示以 $i$ 为右端点的最长回文串,并将 $len_{tr_{p,i}}$ 设置为 $len_p +2$ (此时将奇数的根的 $len$ 设置为 $-1$ 的用处体现出来了,奇数的根下面是单个字符,其长度为 $1$)。
-
继续从 $p$ 往后跳 $fail$ ,直到找到另一个能在前后同时加上 $i$ 的节点, 并将在其前后加上 $i$ 的节点(此处这个节点一定存在,可以尝试根据回文性质自行证明)作为 $tr_{p,i}$ 的 $fail$ 。
-
将 $tr_{p,i}$ 设置为 $lst$ ,准备匹配第 $i+1$ 个字符。


例题:
构建回文自动机,计算每个回文串作为最大后缀回文串出现的次数,再向每个回文串的 $fail$ 贡献出现次数即可。
#include<bits/stdc++.h>
#define N 300010
#define int long long
using namespace std;
char s[N];
int n,ch[N][26],las,p,q,cnt[N*26],len[N*26],fail[N*26],tot;
void init() {
s[0]=-1,fail[0]=1,las=0;
len[0]=0,len[1]=-1,tot=1;
memset(ch,0,sizeof(ch));
}
int nw(int x) {
len[++tot]=x;
return tot;
}
int getfail(int x,int pos) {
while(s[pos-len[x]-1]!=s[pos]) x=fail[x];
return x;
}
void ins(int c,int i) {
p=getfail(las,i);
if(!ch[p][c]) {
q=nw(len[p]+2);
fail[q]=ch[getfail(fail[p],i)][c];
ch[p][c]=q;
}
cnt[las=ch[p][c]]++;
}
int ans=0;
signed main()
{
scanf("%s",s+1),n=strlen(s+1);
init();
for(int i=1; i<=n; i++) ins(s[i]-'a',i);
for(int i=tot; i>=1; i--) cnt[fail[i]]+=cnt[i];
for(int i=1; i<=tot; i++) ans=max(ans,cnt[i]*len[i]);
printf("%lld\n",ans);
return 0;
}
P4762 [CERC2014]Virus synthesis
利用回文自动机查询每个回文串 长度在其一半及以下 的后缀回文串并dp即可。
$Code$
#include<bits/stdc++.h>
#define N 200010
#define sz sizeof(int)*5
using namespace std;
char s[N];
int T,n,val[105],tot;
int dp[N],len[N],fail[N],ch[N][5];
int trans[N],last,p,q,str[N],ans,que[N];
void init() {
val['A']=0,val['C']=1,val['G']=2,val['T']=3;
s[0]=-1,fail[0]=1,last=0;
len[0]=0,len[1]=-1,tot=1;
memset(ch[0],0,sz),memset(ch[1],0,sz);
}
int nw(int x) {
len[++tot]=x;
memset(ch[tot],0,sz);
return tot;
}
int getfail(int x,int n) {
while(s[n-len[x]-1]!=s[n]) x=fail[x];
return x;
}
void ins(int c,int i) {
p=getfail(last,i);
if(!ch[p][c]) {
q=nw(len[p]+2);
fail[q]=ch[getfail(fail[p],i)][c];
ch[p][c]=q;
if(len[q]<=2) trans[q]=fail[q];
else {
int tmp=trans[p];
while(s[i-1-len[tmp]]!=s[i]||(len[tmp]+2)*2>len[q]) tmp=fail[tmp];
trans[q]=ch[tmp][c];
}
}
last=ch[p][c];
}
int main()
{
scanf("%d",&T);
while(T--) {
scanf("%s",s+1);
init();
ans=n=strlen(s+1);
for(int i=1; i<=n; i++) ins(val[s[i]],i);
for(int i=2; i<=tot; i++) dp[i]=len[i];
int head=1,tail=0;que[++tail]=0,dp[0]=1;
while(head<=tail) {
int now=que[head++];
for(int i=0; i<4; i++) {
int x=ch[now][i];
if(!x) continue;
dp[x]=dp[now]+1;
int y=trans[x];
dp[x]=min(dp[x],dp[y]+1+len[x]/2-len[y]);
ans=min(ans,dp[x]+n-len[x]);
que[++tail]=x;
}
}
printf("%d\n",ans);
}
return 0;
}
与上一题类似,对每个回文串记录其长度小于等于其一半的最长后缀回文串,如果其长度恰好为该串的一半,且长度为偶数,向答案贡献即可。
// Problem: P3649 [APIO2014]回文串
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3649
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
#define N 500010
#define int long long
using namespace std;
char s[N];
int n,ch[N][26],las,p,q,cnt[N*26],len[N*26],fail[N*26],tot,trans[N*26];
void init() {
s[0]=-1,fail[0]=1,las=0;
len[0]=0,len[1]=-1,tot=1;
memset(ch,0,sizeof(ch));
}
int nw(int x) {
len[++tot]=x;
return tot;
}
int getfail(int x,int pos) {
while(s[pos-len[x]-1]!=s[pos]) x=fail[x];
return x;
}
void ins(int c,int i) {
p=getfail(las,i);
if(!ch[p][c]) {
q=nw(len[p]+2);
fail[q]=ch[getfail(fail[p],i)][c];
ch[p][c]=q;
if(len[q]<=2) trans[q]=fail[q];
else {
int tmp=trans[p];
while(s[i-1-len[tmp]]!=s[i]||(len[tmp]+2)*2>len[q]) tmp=fail[tmp];
trans[q]=ch[tmp][c];
}
}
cnt[las=ch[p][c]]++;
}
int ans=0;
signed main()
{
scanf("%lld%s",&n,s+1);
init();
for(int i=1; i<=n; i++) ins(s[i]-'a',i);
for(int i=2; i<=tot; i++) if(len[trans[i]]*2==len[i]&&len[trans[i]]%2==0) ans=max(ans,len[i]);
printf("%lld\n",ans);
return 0;
}
从正序和倒序建两个回文自动机,记录以 $i$ 为右端点的最长后缀回文串和以 $i$ 为左端点的最长前缀回文串,枚举分割点即可。
$Code$
//2018.11.21 by ljz
#include<bits/stdc++.h>
using namespace std;
#define res register int
#define LL long long
#define inf 0x3f3f3f3f
#define eps 1e-15
inline int read(){
res s=0;
bool w=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=1;ch=getchar();}
while(ch>='0'&&ch<='9')s=s*10+ch-'0',ch=getchar();
return w?-s:s;
}
inline void _swap(res &x,res &y){
x^=y^=x^=y;
}
inline int _abs(const res &x){
return x>0?x:-x;
}
inline int _max(const res &x,const res &y){
return x>y?x:y;
}
inline int _min(const res &x,const res &y){
return x<y?x:y;
}
const int N=1e5+10;
namespace MAIN{
int n;
int a[N],b[N];
struct PAM{
struct Pam{
int vis[26],len,fail;
}pam[N];
int las,cnt;
PAM() {pam[1].fail=pam[0].fail=1,pam[cnt=1].len=-1;}
inline void extend(const res &x,const res &id,char *str){
res p=las;
for(;str[id-pam[p].len-1]!=str[id];p=pam[p].fail);
if(!pam[p].vis[x]){
res np=++cnt,q=pam[p].fail;
for(;str[id-pam[q].len-1]!=str[id];q=pam[q].fail);
pam[np].fail=pam[q].vis[x],pam[p].vis[x]=np,pam[np].len=pam[p].len+2;
}
las=pam[p].vis[x];
}
}A,B;
char str[N];
int ans;
inline void MAIN(){
scanf("%s",str+1);
n=strlen(str+1);
for(res i=1;i<=n;i++)A.extend(str[i]-'a',i,str),a[i]=A.pam[A.las].len;
reverse(str+1,str+n+1);
for(res i=1;i<=n;i++)B.extend(str[i]-'a',i,str),b[n-i+1]=B.pam[B.las].len;
for(res i=1;i<n;i++)ans=_max(a[i]+b[i+1],ans);
printf("%d\n",ans);
}
}
int main(){
MAIN::MAIN();
return 0;
}

浙公网安备 33010602011771号