


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


例题:
P4762 [CERC2014]Virus synthesis
利用回文自动机查询每个回文串 长度在其一半及以下 的后缀回文串并dp即可。
#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;
}
从正序和倒序建两个回文自动机,记录以 为右端点的最长后缀回文串和以 为左端点的最长前缀回文串,枚举分割点即可。
//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;
}
