标签:
求解字符串中最长的回文子串。
印象中以前是做过的,用的是动态规划的区间求法,还有取中心的方法,时间复杂度O(N)的Manacher竟是闻所未闻,好好学习了一下,为之拍案叫绝。于是现在把最长回文子串的问题做个总结,以便回头翻看。
一、枚举法
枚举所有子串(O(N2)),判断是否为回文串(O(N)),时间复杂度为O(N3),不表。
二、取中心点法
枚举每一个字符str[i]看做回文子串的中心,向两边扩散,比较左端点是否等于右端点,得出每个字符做中心的最大回文长度,枚举答案即可。时间复杂度为O(N2),这个方法的好处在于好理解,看着比较舒服,本质还是枚举,枚举的一种优化嘛。但是毕竟只是O(N2)的效率,不是最优。
附代码:
#include<stdio.h> #include<string.h> char str[50005]; int a[50005],n; int func1(int k) { int i,j,sum=1; if(k==0)return sum; if(k==n-1)return sum; i=k-1;j=k+1; while(i>=0 && j<n) { if(str[i]!=str[j])return sum; sum+=2; i--;j++; } return sum; } int func2(int k) { if(k==0)return 1; if(k==n-1)return 1; if(str[k]!=str[k+1])return 0; int i,j,sum=2; i=k-1;j=k+2; while(i>=0 && j<n) { if(str[i]!=str[j])return sum; sum+=2; i--;j++; } return sum; } int main() { int lp,i,j,k,sum,ans,tot,m; scanf("%d",&m); for(lp=1;lp<=m;lp++) { scanf("%s",str); n=strlen(str); memset(a,0,sizeof(a)); for(i=0;i<n;i++) { j=func1(i); k=func2(i); a[i]=j>k?j:k; } ans=0; for(i=0;i<n;i++) if(a[i]>ans)ans=a[i]; printf("%d\n",ans); } }
func1()和func2()对应着奇数回文串和偶数回文串,这样写比较顺手。总之这个方法还是比较清爽的。
三、区间动归
大爱dp。
说一下动归。f[i][j]表示从i到j的子串是否是回文串,两个状态f[i][j]=1,f[i][j]=0,
边界是:
f[i][i]=1
if(str[i]==str[i+1])f[i][i+1]=1;即偶数回文串
状态转移方程则是
f[i][j]=f[i+1][j-1]&&str[i]==str[j]
求一下长度就好了。
时间复杂度O(N2)。
#include<stdio.h> #include<string.h> int f[2010][2010],n; char str[2010]; int main() { int lp,i,j,k,m,ans=0,len; scanf("%d",&m); for(lp=0;lp<m;lp++) { scanf("%s",str); memset(f,0,sizeof(f)); n=strlen(str); ans=0; for(i=0;i<n;i++) { f[i][i]=1; if(i<n-1&&str[i]==str[i+1])f[i][i+1]=1; } for(len=3;len<=n;len++) for(i=0;i<=n-len;i++) { j=i+len-1; if(f[i+1][j-1]&&str[i]==str[j]) { ans=len; f[i][j]=1; } } printf("%d\n",ans); } }
重点来了。
四、Manacher法
时间复杂度O(N)!真是太美好了。我在理解上面有一点点卡,代码实现比较容易,非常好的方法。
Manacher的方法是:枚举每一个点为中心的最长回文子串,在计算未知点 i 时,使用一个已知的、足够长(可以覆盖到 i 点)的回文子串(这个已知子串的中心点记为id),找出与 i 在id子串上对称的点 j(也就是以id为中心,找出一个 i 点的轴线对称点),由于这个 j 是已知的,i 和 j 又都被 id子串覆盖,所以 i 点的回文长度 应该不小于 j 点。这里面有两种情况,一种是以 j 为中心的回文串的最长长度也没有超过id子串的覆盖范围,那么 i 点可以直接取 j 的长度值了;另一种是 j 的最长长度超出id子串的覆盖范围,那么由于回文串的对称性,我们可以知道 i 点至少在id串的覆盖范围内是可以确定为回文的,这时候 i 点应该取从 i 点到id串范围的长度。具体内容如下。
我们用一个数组p在储存每一个字符为中心的最长回文串的半径,id表示待用的节点,mx表示id串最右边的边缘位置。
举个例子
0 1 2 3 4 5 6 7 8 9 10
a c f c b a b c f c d
待求点 i = 8 , id = 5 , mx = id+p[id]=10 , i+j=2*id , j = 2 ,p[j]=2
这个例子中,id刚好覆盖到 i,j 两点 我们直接取p[i]=p[j]即可
如果 str[0]=b,那么 j 的子串是[0..4], id的子串[1..9]没有完全覆盖,我们由对称性可以得出,i 点至少到 9这个位置的回文性质是可以确定的,10这个位置是否对称还不知道,故我们只能取 i 点到mx , 即p[i]=mx-i; 概括地讲那就是 p[i]=min{p[j],mx-i};这是p[i]最小值,然后再在这个基础上扩展一下即可得出正确结果。
这里有一个问题要注意,那就是偶数型的回文串怎么办呢,并没有一个中心点。于是,我们在初始化时,在字符串中插入了一些容易区分的字符,比如‘#‘,这样一来,一个abba的回文串,变成 #a#b#b#a#,那么这个回文串变成了一个以‘#‘为中心的回文串,将奇数回文串和偶数回文串统一起来。有意思的是,增加之后,p[i]-1就等于原字符串的长度,这个可以自己画一画,很容易看出。
#include<stdio.h> #include<string.h> int p[2200010],n; char str[2200010],s[2200010]; void init() { int i,j,k; n=strlen(s); str[0]=‘$‘; str[1]=‘#‘; for(i=0;i<n;i++) { str[i*2+2]=s[i]; str[i*2+3]=‘#‘; } n=n*2+2; s[n]=0; } void manacher() { int i,j,mx=0,id=1; for(i=1;i<n;i++) { j=2*id-i; if(mx>i) p[i]=p[j]<(mx-i)?p[j]:(mx-i); else p[i]=1; for(;str[i+p[i]]==str[i-p[i]];p[i]++) ; if(i+p[i]>mx) { mx=i+p[i]; id=i; } } } int main() { int i,j,k,ans,lp,m; scanf("%d",&m); for(lp=0;lp<m;lp++) { scanf("%s",s); memset(p,0,sizeof(p)); init(); manacher(); ans=0; for(i=1;i<n;i++) if(ans<p[i])ans=p[i]; printf("%d\n",ans-1); } }
虽然有循环嵌套,但这两个循环不遵循乘法原理,内层循环只扫未确定的区域,所以时间复杂度为O(N)。
酸爽!
标签:
原文地址:http://www.cnblogs.com/puszeto/p/4375512.html