Longest Palindromic Substring
题意:找出一个字符串中最长的回文子串
解法:
O(N^2):DP解法:
以f[i][j]代表字符串第i到第j个字符组成的子串是否是回文。
若a[i]==a[j],则f[i][j]=f[i+1][j-1],否则f[i][j]=false
最外层循环枚举子串的长度
时刻记录最长的回文子串的长度,若在循环某个长度i时,发现最长的回文子串长度<i-2,则这一层不可能有回文子串,程序结束。
public class Solution147 {
public String longestPalindrome(String s) {
if (s.length()==0){
return s;
}
if (s.length()==1){
return s;
}
if (s.length()==2){
if (s.charAt(0)==s.charAt(1)){
return s;
}else{
returns.substring(1);
}
}
int n=s.length();
boolean[][] f=new boolean[n][n];
for (int i=0;i<n;i++){
f[i][i]=true;
}
int maxLength=1;
String ans="";
for (int i=0;i<n-1;i++){
if(s.charAt(i)==s.charAt(i+1)){
f[i][i+1]=true;
if(2>maxLength){
maxLength=2;
ans=s.substring(i,i+2);
}
}
}
for (int i=3;i<=n;i++){
if (maxLength<i-2){
break;
}
for (intj=0;j+i-1<=n-1;j++){
if(s.charAt(j)==s.charAt(j+i-1)){
f[j][j+i-1]=f[j+1][j+i-2];
if(f[j][j+i-1]){
if(i>maxLength){
maxLength=i;
ans=s.substring(j,j+i);
}
}
}else{
f[j][j+i-1]=false;
}
}
}
return ans;
}
}
O(N):参考http://blog.csdn.net/hopeztm/article/details/7932245
原文写得还算清楚,这里我用我的理解解释一遍这个叫Manacher’s Algorithm的算法。下面前两张图为引用
算法第一步:将原来字符串XXXXX扩充成#X#X#X#X#X#,就是在所有字符之间加一个#,字符串前后各加一个#。在原来字符串中的每一种回文可能,在新串中都有一种对应,奇数长度的回文串,长度d,表示为以某非#字符为中心的,左右各向外扩展d的新串。偶数长度的回文串,长度d,表示为以某#字符为中心的,左右各向外扩展d的新串。举例,原来abcba,新串的某一段一定为#a#b#c#b#a#,以c为中心,左右各延展5个长度。再举例,原来abba,新串的某一段一定为#a#b#b#a#,以#为中心,左右各延展4个长度。我们以数组p来记录新串中每一个字符可以延展的最长长度。那么p最后的最大值,即为原串的最长回文的长度。
在计算p时,最直观暴力的方法是,对新串中的每一个字符,向外扩展,直到扩展不了。这个过程和在原串中的暴力解法是一样的。但我们观察如下某个情况,会发现在所有p的计算中,存在曾经的p值可以被后来p值所利用情况。看下图
图中i为我们当前要计算的某个字符的位置,如果我们已经某个较长的回文中心所在的位置,例如C,p[C]=11,意味着从C位置-11到C位置+11,是个回文。而i关于回文中心C的镜像位置I’的p值为1,那么意味着p[i]一定为1,因为i和I’同样位于整个大回文串中,I’所能达成最长回文被包含在回文串中,i的情况一定与I’完全镜像。
但我们所说的前提I’的最长回文被包含在最长回文里,如果不完全包括的话,看下图。
当计算i=15时,I’的p值为7,意味着以I’为中心的最长回文有一部分超出在目前大回文的范围,那么这一部分首先被我们舍弃,但大回文的右边界20与i=15的差的部分,很明显是以i为回文中心的右侧。也就是说当I’的值大于这个R-i时,我们就认为I’的值为R-i好了,这样一定不会出错。但究竟大于20的部分,会不会也是以i为中心的回文的一部分呢,我们继续循环判断就好了。
说到这里,将会产生疑问,那么这个大的回文串(图中以C为中心,位置2到位置20)该如何维护呢,我们在从左到右求p[i]的过程中,该选取i左边的哪个大回文串呢?
首先,大回文串一定要包含i,否则计算p[i]一定用不上它的镜像信息。
那是否意味着,要维护左边所有的大回文串?
这里原算法给出非常精妙的选择方法,全局只维护这样一个大回文串,每当算出一个i的回文串时,若i的最长回文串的右边界大于目前大回文串的右边界,就舍弃原来的大回文串,记录以i为中心,向左向右各为p[i]的回文串为新的大回文串。
这里给出一张我画的图。
I代表要计算p值的点,目前已经得到的所有可以利用到大回文串,我们只用两个来说明,一个是以A为中心,从A1到A2的回文串,一个是以B为中心,从B1到B2的回文串。在计算p[i]时,从A1-A2可以利用的信息最多是从i到A2,从B1-B2可以利用的信息最多是从i到B2,明显的包含关系,说明以前所有的大回文串中,只需要记录右边界最靠右的就可以了,而与这个回文串的总长度无关。如果说能从A1-A2中得到i-A2这段信息,那么从B1-B2可以一定可以获得。
算法的时间复杂度分析:我们的目的是计算p[i],而在计算个别p[i]时,存在需要向右拓展来判断以i为中心的回文是否延伸到目前大回文串的外面。而这个延伸过程,每向外延伸一个距离,最终都会使目前维护的大回文串的右边界向右拓展一步,而大回文串的右侧最多可以延展n次,所以我们对所有i的延伸判断最多有n次,综上所述,这个算法的时间复杂度为Θ(N)。
另外,本题要求返回的是原来字符串的最长回文子串,只要将新串得到的最大p值和对应index通过简单计算即可得到原来字符串对应最长回文子串的初始位置了。
public class Solution147second {
publicString longestPalindrome(String s) {
if(s.length()==0){
return"";
}
if(s.length()==1){
returns;
}
char[]sTemp=new char[s.length()*2+1];
sTemp[0]='#';
inthigh=1;
for(int i=0;i<s.length();i++){
sTemp[high]=s.charAt(i);
high++;
sTemp[high]='#';
high++;
}
int[]p=new int[s.length()*2+1];
intc=0;
intr=0;
for(int i=1;i<s.length()*2+1;i++){
intiMirror=c-(i-c);
if(r>i){
p[i]=Math.min(r-i,p[iMirror]);
}else{
p[i]=0;
}
while((i-p[i]-1>=0)&&(i+p[i]+1<=s.length()*2)&&sTemp[i-p[i]-1]==sTemp[i+p[i]+1]){
p[i]++;
}
if(i+p[i]>r){
c=i;
r=i+p[i];
}
}
intmax=0;
intindex=-1;
for(int i=0;i<2*s.length()+1;i++){
if(p[i]>max){
max=p[i];
index=i;
}
}
index=index/2-max/2;
returns.substring(index, index+max);
}
}
以下可能是一种错误的算法:
另一种O(N^2)的解法:
同样参考http://blog.csdn.net/hopeztm/article/details/7932245
KMP匹配,由于复杂度并没有比DP强,所以不做过多介绍,况且实现KMP也是很麻烦的事情。
Ps:按照原文算法,abcdba和abdcba进行匹配时,可以匹配到ab。但原String根本没有回文。所以我认为该算法是不正确的。