正题
题目链接:https://www.luogu.com.cn/problem/P4762
题目大意
长度为nnn的目标串,开始一个空串,可以执行以下操作
- 在头或者尾加一个字符
- 复制一个该串的逆串放在后面
求最少操作次数。
解题思路
我们可以知道答案肯定是一个回文串然后剩下的暴力加上。
我们构建一个PAMPAMPAM,然后用fif_{i}fi表示带该回文串需要的最少次数。
对于一个节点的转移有fy=min{fx+1}f_{y}=min\{f_{x}+1\}fy=min{fx+1}
就是该回文串头尾各加上一个字符。
该回文串还有可能是一个双倍回文,即一个回文串在复制一个逆串后回文,那么有我们要像[SHOI2011]双倍回文这道题目一样维护一个最长的不超过该串一半的回文后缀的节点halfihalf_ihalfi。
然后有转移
fx=min{fhalfx+1+lenx2−lenhalfx}f_{x}=min\{f_{half_x}+1+\frac{len_x}{2}-len_{half_x}\}fx=min{fhalfx+1+2lenx−lenhalfx}
然后答案就是min{n−lenx+fx}min\{n-len_x+f_{x}\}min{n−lenx+fx}
时间复杂度O(4Tn)O(4Tn)O(4Tn)
如果用stdstdstd自带队列会比较慢需要自行卡常。
codecodecode
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
queue<int> q;
const int N=1e5+10;
int T,n,m,half[N],f[N],ans;
int len[N],fail[N],next[N][4],cnt;
char s[N];
int z(char x){if(x=='A') return 0;if(x=='C') return 1;if(x=='G') return 2;if(x=='T') return 3;
}
int get_fail(int x,int n){while(s[n-len[x]-1]!=s[n])x=fail[x];return x;
}
void Make_PAM(){int last=0;len[1]=-1;s[0]='#';cnt=fail[0]=1;for(int i=1;i<=n;i++){int val=z(s[i]),x=get_fail(last,i);if(!next[x][val]){len[++cnt]=len[x]+2;int y=get_fail(fail[x],i);fail[cnt]=next[y][val];if(len[cnt]<=2) half[cnt]=fail[cnt];else{int z=half[x];while(s[i-len[z]-1]!=s[i]||((len[z]+2)<<1)>len[cnt])z=fail[z];half[cnt]=next[z][val];}next[x][val]=cnt;}last=next[x][val];}return;
}
void Solve(){ans=n;for(int i=2;i<=cnt;i++)f[i]=len[i];f[0]=1;q.push(0);while(!q.empty()){int x=q.front();q.pop();f[x]=min(f[x],f[half[x]]+1+len[x]/2-len[half[x]]);ans=min(ans,n-len[x]+f[x]);for(int i=0;i<4;i++){int y=next[x][i];if(!y) continue;f[y]=min(f[y],f[x]+1);q.push(y);}}return;
}
int main()
{scanf("%d",&T);while(T--){for(int i=0;i<=cnt;i++)for(int j=0;j<4;j++)next[i][j]=0;scanf("%s",s+1);n=strlen(s+1);Make_PAM();Solve();printf("%d\n",ans);}return 0;
}