<学习笔记> manacher 和 回文自动机
manacher
对于奇串,考虑维护一个具有最大 \(r\) 值的回文串 \((l,r)\),每次考虑一个新增加的点 \(i\)。若 \(i > r\) 那么直接暴力计算就可以;如果 \(i \leq n\),设 \(mid=(l+r)/2\),那么 \(i\) 点关于 \(mid\) 对称点 \(r+l-i\) 的回文串长度同样适用于 \(i\),那么直接在此基础上更新就可以,奇偶同理。
code
int manacher(int n){
for(int i=1,l=0,r=-1;i<=n;i++){
int k;
if(i>r) k=1;
else k=min(r-i+1,f[r+l-i]);
while( i-k>=1 && i+k<=n &&s[i-k]==s[i+k]) k++;
f[i]=k--;
ans=max(ans,f[i]*2-1);
if(r<i+k){
r=i+k;
l=i-k;
}
}
for(int i=1,l=0,r=-1;i<=n;i++){
int k;
if(i>r) k=0;
else k=min(r-i+1,g[l+r-i+1]);
while(i-k-1>=1 && i+k<=n && s[i-k-1]==s[i+k]) k++;
g[i]=k--;
ans=max(ans,g[i]*2);
if(r<i+k){
r=i+k;
l=i-k-1;
}
}
return ans;
}
回文自动机(回文树,PAM)
对于一个新加入的字符 \(s_i\),新产生的本质不同的串一定是新串的一个后缀,而且还是最长的后缀,因为如果不是最长的话,可以被最长的后缀中点对应到插入前的串中。可以发现每次新增最多为 \(1\)。那么我们可以根据这个性质,定义一个 \(fail[x]\) 表示为 \(x\) 代表的回文子串的最长回文后缀。
对于每个节点表示的回文串其实就是:从它到根再从根到它。
构建
为了区分奇偶串,建两个根 \(0,1\), \(0\) 为偶数根。定义 \(len[i]\) 为以 \(i\) 结尾的回文串的长度。\(len[1]=-1\).
\(fail[0]=1\),奇数根可以挂,因为单独可以成为一个回文串。
每加入一个字符,在当前最长后缀回文串中找到最长的可以匹配新加字符的回文串,如果没有这个节点就新增一个节点,这样本质不同回文串个数加一,并更新 \(fail\)。
code
char s[N];
int len[N],fail[N],num[N],tr[N][26],tot=1,cur=0,pos=0;
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 solve(){
int n=strlen(s+1);
fail[0]=1;
len[1]=-1;
int last=0;
for(int i=1;i<=n;i++){
pos=getfail(cur,i);
if(!tr[pos][s[i]-'a']){
fail[++tot]=tr[getfail(fail[pos],i)][s[i]-'a'];// 注意是 getfail(fail[pos],i),
// 如果是 getfail(pos,i) 就会匹配自己
tr[pos][s[i]-'a']=tot;
len[tot]=len[pos]+2;
num[tot]=num[fail[tot]]+1;
}
cur=tr[pos][s[i]-'a'];
}
}
例题
双倍回文
PAM做法:其实可以贡献的回文串是它存在一个后缀回文串长度为它的 \(\frac{1}{2}\)。所以在建 \(PAM\) 时每增加一个新串时,可以暴跳 \(fail\) 查看是否存在。这样复杂度是错的,但是 \(oj\) 可过。其实可以建一课 \(fail\) 树,我们遍历这棵树的时候开一个桶,维护长度出现次数就可以了。
code
#include<bits/stdc++.h>
using namespace std;
const int N=5*1e5+10;
char s[N];
int fail[N],len[N],tr[N][26],tot=1,pos=0,cur=0;
int get_fail(int x,int i){
while(i-len[x]-1<=0 || s[i-len[x]-1]!=s[i]) x=fail[x];
return x;
}
int head[N*2],nex[N*2],ver[N*2],idx=0;
void add(int x,int y){
ver[++idx]=y,nex[idx]=head[x],head[x]=idx;
}
int cnt[N];
int ans;
void dfs(int x){
cnt[len[x]]++;
if(len[x]%4==0){
if(cnt[len[x]/2]) ans=max(ans,len[x]);
}
for(int i=head[x];i;i=nex[i]){
int y=ver[i];
dfs(y);
}
cnt[len[x]]--;
}
signed main(){
int n;
scanf("%d",&n);
scanf("%s",s+1);
fail[0]=1,len[1]=-1;
add(1,0);
ans=0;
for(int i=1;i<=n;i++){
pos=get_fail(cur,i);
if(!tr[pos][s[i]-'a']){
fail[++tot]=tr[get_fail(fail[pos],i)][s[i]-'a'];
add(fail[tot],tot);
tr[pos][s[i]-'a']=tot;
len[tot]=len[pos]+2;
}
cur=tr[pos][s[i]-'a'];
}
dfs(1);
printf("%d",ans);
}
P4762 [CERC2014] Virus synthesis
可以发现加的逆串越多越优。
然后发现一个串加逆串就构成一个回文串,而且贡献就是一个回文串加上剩余的。设 \(f[i]\) 表示构成 \(i\) 这个回文串的最小次数,那么答案就是最小的 \(n-len[i]+f[i]\)。
考虑有什么转移,假如树上有条边 \(y -> x\) 那么 \(f[x]=min(f[y]+1)\)。还有什么情况就是设这个串后缀长度 \(\leq len[x]\) 的节点 \(trans[x]\) 那么有转移 \(f[x]=min(len[x]/2-len[trans[x]]+f[trans[x]]+1)\)。对于 \(trans[x]\) 的转移可以每次从 \(trans[y]\) 暴跳。最后建完树,一层一层更新就行。
code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
char s[N];
int tr[N][5],fail[N],len[N],pos,cur,f[N],tot=1;
int trans[N],rk[N];
int getfail(int x,int i){
while(i-len[x]-1<=0 || s[i-len[x]-1]!=s[i]) x=fail[x];
return x;
}
signed main(){
int T;
scanf("%d",&T);
rk['A']=0,rk['T']=1,rk['C']=2,rk['G']=3;
while(T--){
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=0;i<=n+2;i++){
fail[i]=len[i]=trans[i]=0;
f[i]=(1<<25);
tr[i][0]=tr[i][1]=tr[i][2]=tr[i][3]=0;
}
tot=1,pos=cur=0;
fail[0]=1,len[1]=-1;
int ans=n;
f[0]=f[1]=1;
for(int i=1;i<=n;i++){
pos=getfail(cur,i);
if(!tr[pos][rk[s[i]]]){
fail[++tot]=tr[getfail(fail[pos],i)][rk[s[i]]];
tr[pos][rk[s[i]]]=tot;
len[tot]=len[pos]+2;
if(len[tot]<=2) trans[tot]=fail[tot];
else{
int tmp=trans[pos];
while(s[i-len[tmp]-1]!=s[i] || (len[tmp]+2)*2>len[tot]) tmp=fail[tmp];
trans[tot]=tr[tmp][rk[s[i]]];
}
}
if(len[tot]%2==0){
int tmp=trans[tot];
f[tot]=min(f[pos]+1,f[tot]);
f[tot]=min(f[tot],f[tmp]+len[tot]/2-len[tmp]+1);
ans=min(ans,f[tot]+n-len[tot]);
}
cur=tr[pos][rk[s[i]]];
}
for(int i=2;i<=n;i++) f[i]=len[i];
queue<int> q;
for(int i=0;i<4;i++){
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()){
int x=q.front();
q.pop();
f[x]=min(f[x],len[x]/2-len[trans[x]]+f[trans[x]]+1);
for(int i=0;i<4;i++){
if(tr[x][i]){
q.push(tr[x][i]);
f[tr[x][i]]=min(f[x]+1,f[tr[x][i]]);
}
}
ans=min(ans,f[x]+n-len[x]);
}
printf("%d\n",ans);
}
}