KMP&扩展KMP&Manacher算法基础与习题(第一更)
KMP&扩展KMP&Manacher算法基础与习题(第二更)
目录
Manacher算法讲解(转自Manacher算法讲解)
算法思路
首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#(注意,下面的代码是用C语言写就,由于C语言规范还要求字符串末尾有一个' '所以正好OK,但其他语言可能会导致越界)。
下面以字符串12212321为例,经过上一步,变成了 S[] = "$#1#2#2#1#2#3#2#1#";
然后用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:
S # 1 # 2 # 2 # 1 # 2 # 3 # 2 # 1 #
P 1 2 1 2 5 2 1 4 1 2 1 6 1 2 1 2 1
(p.s. 可以看出,P[i]-1正好是原字符串中回文串的总长度)
那么怎么计算P[i]呢?该算法增加两个辅助变量(其实一个就够了,两个更清晰)id和mx,其中 id 为已知的 {右边界最大} 的回文子串的中心,mx则为id+P[id],也就是这个子串的右边界。
然后可以得到一个非常神奇的结论,这个算法的关键点就在这里了:如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。就是这个串卡了我非常久。实际上如果把它写得复杂一点,理解起来会简单很多:
//记j = 2 * id - i,也就是说 j 是 i 关于 id 的对称点(j = id - (i - id))
if (mx - i > P[j])
P[i] = P[j];
else /* P[j] >= mx - i */
P[i] = mx - i; // P[i] >= mx - i,取最小值,之后再匹配更新。
当然光看代码还是不够清晰,还是借助图来理解比较容易。
当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图。
当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。
对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。
于是代码如下:
//输入,并处理得到字符串s
int p[1000], mx = 0, id = 0;
memset(p, 0, sizeof(p));
for (i = 1; s[i] != ' '; i++) {
p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
while (s[i + p[i]] == s[i - p[i]]) p[i]++;
if (i + p[i] > mx) {
mx = i + p[i];
id = i;
}
}
//找出p[i]中最大的
模板
int init(){//对原字符进行预处理
newStr[0]='$';
newStr[1]='#';
int j=2;
int len=strlen(str);
for (int i=0;i<len;i++){
newStr[j++]=str[i];
newStr[j++]='#';
}
newStr[j] =' '; //字符串结束标记
return j;//返回newStr的长度
}
int manacher(){
int len=init();//取得新字符串长度并完成字符串的预处理
int res=-1;//最长回文长度
int id;
int mx=0;
for(int i=1;i<len;i++){
int j=2*id-i;//与i相对称的位置
if(i<mx)
p[i]=min(p[j], mx-i);
else
p[i]=1;
//由于左有'$',右有' ',不需边界判断
while(newStr[i-p[i]] == newStr[i+p[i]])//p[i]的扩大
p[i]++;
if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
id=i;
mx=i+p[i];
}
res=max(res,p[i]-1);
}
return res;
}
例题
A:HDU-3613 Best Reward:题目的要求就是给你一个字符串让你把它分成两个字符串 ,一行给你26个字母的价值,如果分出来的子串是回文序列,那么它的价值就是序列所有字母价值的和,如果不是回文序列则价值为0,找最大的价值,扩展kmp:将母串s1分为T1,T2两个子串(T1为前半串,T2为后半串) 首先找到s1的倒串s2;用s1去匹配s2,判断T1是不是回文序列,通过s2匹配s1判断T2是不是回文序列。AC代码:
#include<stdio.h>
#include<string>
#include<string.h>
#include<iostream>
#include<algorithm>
using namespace std;
int v[27];
char s1[500005];
char s2[500005];
int nex[500005];
int ex1[500005];
int ex2[500005];
int sum[500005];
void Getnext(char *str)
{
int i=0,j,po,len=strlen(str);
nex[0]=len;
while(str[i]==str[i+1]&&i+1<len)
i++;
nex[1]=i;
po=1;
for(i=2;i<len;i++)
{
if(nex[i-po]+i<nex[po]+po)
nex[i]=nex[i-po];
else
{
j=nex[po]+po-i;
if(j<0)j=0;
while(i+j<len&&str[i+j]==str[j])
j++;
nex[i]=j;
po=i;
}
}
}
void EXKMP(char *s1,char *s2,int ex[])
{
int i=0,j,po,len=strlen(s1),l2=strlen(s2);
Getnext(s2);
while(s1[i]==s2[i]&&i<len&&i<l2)
i++;
ex[0]=i;
po=0;
for(i=1;i<len;i++)
{
if(nex[i-po]+i<ex[po]+po)
ex[i]=nex[i-po];
else
{
j=ex[po]+po-i;
if(j<0)j=0;
while(i+j<len&&j<l2&&s1[i+j]==s2[j])
j++;
ex[i]=j;
po=i;
}
}
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
for(int i=0;i<26;i++)
scanf("%d",&v[i]);
scanf("%s",s1);
int len=strlen(s1);
sum[0]=0;
for(int i=0;i<len;i++)
{
s2[i]=s1[len-i-1];
sum[i+1]=sum[i]+v[s1[i]-'a'];
}
EXKMP(s2,s1,ex1);
EXKMP(s1,s2,ex2);
int ans=-500000;
for(int i=1;i<len;i++)//计算价值
{
int tmp=0;
if(i+ex1[i]==len)tmp+=sum[len-i];/*从i处分开,如果i(T2)+ex[i](T1)==len,说明T1是回文,T1的长度是(len-i),T1是前半部分所以价值是 sum[len-i];*/
int pos=len-i;
if(pos+ex2[pos]==len)tmp+=sum[len]-sum[pos];/*pos是T1的长度,如果pos(T1)+ex2[pos](T2)==len,说明T2是回文,T2是后半部分,所以价值是sum[len]-sum[pos]*/
if(tmp>ans)
ans=tmp;
}
printf("%d
",ans);
}
}
B:POJ-3974 Palindrome:求最大回文的长度,Manacher的模板题,AC代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
#define INF 0x3f3f3f3f
#define N 1000001
using namespace std;
char str[N];//原字符串
char newStr[N*2];//预处理后的字符串
int p[N*2];//辅助数组
int init(){//对原字符进行预处理
newStr[0]='$';
newStr[1]='#';
int j=2;
int len=strlen(str);
for (int i=0;i<len;i++){
newStr[j++]=str[i];
newStr[j++]='#';
}
newStr[j] =' '; //字符串结束标记
return j;//返回newStr的长度
}
int manacher(){
int len=init();//取得新字符串长度并完成字符串的预处理
int res=-1;//最长回文长度
int id;
int mx=0;
for(int i=1;i<len;i++){
int j=2*id-i;//与i相对称的位置
if(i<mx)
p[i]=min(p[j], mx-i);
else
p[i]=1;
//由于左有'$',右有' ',不需边界判断
while(newStr[i-p[i]] == newStr[i+p[i]])//p[i]的扩大
p[i]++;
if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
id=i;
mx=i+p[i];
}
res=max(res,p[i]-1);
}
return res;
}
int main(){
int Case=1;
while(scanf("%s",str)!=EOF){
if(str[0]=='E')
break;
printf("Case %d: %d
",Case++,manacher());
}
return 0;
}
C:HDU-4513 吉哥系列故事——完美队形II:在manacher函数中加一个判断,跳过原来的加入的值,以及加一个判断控制最中间向两边满足非递增即可。具体参考代码。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define N 1000001
using namespace std;
int str[N];//原字符串
int newStr[N*2];//预处理后的字符串
int p[N*2];//辅助数组
int n;
int init(){//对原字符进行预处理
newStr[0]=-1;
newStr[1]=-2;
int j=2;
int len=n;
for (int i=0;i<len;i++){
newStr[j++]=str[i];
newStr[j++]=-2;
}
newStr[j] =' '; //字符串结束标记
return j;//返回newStr的长度
}
int manacher(){
int len=init();//取得新字符串长度并完成字符串的预处理
int res=-1;//最长回文长度
int id;
int mx=0;
for(int i=1;i<len;i++){
int j=2*id-i;//与i相对称的位置
if(i<mx)
p[i]=min(p[j], mx-i);
else
p[i]=1;
//由于左有'$',右有' ',不需边界判断
while(newStr[i-p[i]] == newStr[i+p[i]]){//p[i]的扩大
if(newStr[i+p[i]]!=-2){
if(newStr[i+p[i]]<=newStr[i+p[i]-2])
p[i]++;
else
break;
}
p[i]++;
}
if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
id=i;
mx=i+p[i];
}
res=max(res,p[i]-1);
}
return res;
}
int main(){
int t;
cin>>t;
while(t--){
cin>>n;
for(int i=0;i<n;i++)
scanf("%d",&str[i]);
cout<<manacher()<<endl;
}
return 0;
}
D:HDU-3294 Girls' research:这道题的主要意思是给你第一个字母代表a,让你递推出这串字符真正的字符串是什么,然后叫你找出字符串里面回文串的开始与结束下标,然后打印出这串回文,主要是要知道新字符串在原本字符串中对应的位置,具体见代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
#define INF 0x3f3f3f3f
#define N 1000001
using namespace std;
char str[N];//原字符串
char newStr[N*2];//预处理后的字符串
int p[N*2];//辅助数组
int t;
int init(){//对原字符进行预处理
newStr[0]='$';
newStr[1]='#';
int j=2;
int len=strlen(str);
for (int i=0;i<len;i++){
newStr[j++]=str[i];
newStr[j++]='#';
}
newStr[j] =' '; //字符串结束标记
return j;//返回newStr的长度
}
int manacher(){
int len=init();//取得新字符串长度并完成字符串的预处理
int res=-1;//最长回文长度
int id;
int mx=0;
for(int i=1;i<len;i++){
int j=2*id-i;//与i相对称的位置
if(i<mx)
p[i]=min(p[j], mx-i);
else
p[i]=1;
//由于左有'$',右有' ',不需边界判断
while(newStr[i-p[i]] == newStr[i+p[i]])//p[i]的扩大
p[i]++;
if(mx<i+p[i]){//由于希望mx尽可能的远,因此要不断进行比较更新
id=i;
mx=i+p[i];
}
if(p[i]-1>res){
res=p[i]-1;
t=i;
}
}
return res;
}
int main(){
char n;
while(scanf("%c",&n)==1){
scanf("%s",str);
int len=strlen(str);
for(int i=0;i<len;i++){
if(str[i]>=n){
str[i]=97+(str[i]-n);//把比b[i]大于等于的转化
}
else{
str[i]=123-(n-str[i]);//把比b[i]小的转化
}
}
getchar();//注意这个很重要,我检查了半天才发现,否则第二次输入时会转化成一堆乱码
int max1=manacher();
if(max1>1){
if(t%2==1){//分为两种情况对待,优势p[t]最大的时候刚好指着#号
printf("%d %d
",(t-1)/2-(max1)/2,(t-1)/2+(max1)/2-1);
for(int i=(t-1)/2-(max1)/2;i<=(t-1)/2+(max1)/2-1;i++) printf("%c",str[i]);
printf("
");
}
else{
printf("%d %d
",t/2-(max1)/2-1,t/2+(max1)/2-1);
for(int i=t/2-(max1)/2-1;i<=t/2+(max1)/2-1;i++) printf("%c",str[i]);
printf("
");
}
}
else printf("No solution!
");
}
return 0;
}
E:HDU-4763 Theme Section:给出一串主串,让求主串中EAEBA形式的E串的最大长度,必须要开头E串结尾E串,AB串长度任意,可以是0;思路:既然要求三个相同的子串,而且有两个还必须在开头和结尾,那就求Next数组,Next数组存的是前后缀相同的长度,所以只需要找【2*i,L-Next[i]】之间相同的字串,即Next[j]==i;具体代码:
#include<stdio.h>
#include<string.h>
#include<string>
#include<algorithm>
#include<math.h>
#include<map>
#include<queue>
#include<vector>
#include<stack>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
const int N=1000005;
char s[N];
int Next[N],l;
void get_Next()
{
int i=0,j=-1;
Next[0]=-1;
while(i<l)
{
if(j==-1||s[i]==s[j])
Next[++i]=++j;
else
j=Next[j];
}
}
int KMP()
{
int i,j;
for(i=Next[l]; i; i=Next[i])
for(j=2*i; j<=l-i; j++)
if(Next[j]==i)
return i;
return 0;
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
scanf("%s",s);
l=strlen(s);
get_Next();
printf("%d
",KMP());
}
return 0;
}