一般的求最长回文子串,我喜欢用两种方法。
当n不是很大时,我们可以用O(n^2)的方法, 即枚举中心点,然后向两边进行比较,当然长度为偶数的回文串是没有中心点的,所以我们得按照奇数和偶数两种进行求解。
char s[N];
void findLongestPalindrome(char *s){ //以枚举回文串的中心点来寻找回文串,时间复杂度(On^2),要分长度为奇偶两种情况
int len = strlen(s);
int maxlen = 0;
int st;
for(int i=0;i<len;i++){ //回文串长度为奇数的时候
int j = i-1;k=i+1;
while(j>=0&&k<len&&s[j]==s[k]){
if(k-j+1>maxlen){
maxlen = k-j+1;
st = j;
}
j--;
k++;
}
}
for(int i=0;i<len;i++){ //回文串为偶数的时候
int j = i;k = i+1;
while(j>=0 && k<len && s[j] == s[k]){
if(k-j+1>maxlen){
maxlen = k-j+1;
st = j;
}
j--;
k++;
}
}
// for(int i=st;i<st+maxlen;i++){
// printf("%c",s[i]);
// }
printf("%d\n",maxlen);
}
但是对于这道题目来说,n有10^6,所以n^2效率肯定是不行的,这个数量级的基本也就是用O(n)效率算法来解决了。
这种算法被称为manacher算法。
manacher算法能达到O(n)的原因是因为它定义了一个数组p[i],代表以第i个字符为中心,最长回文子串的半径为p[i],神奇的是,后面的p[i]可以利用前面已经算出的p[i]进行推导。
先给出Mancher算法的核心代码:
void findLongestPalindrome(char *s){ //
int len = strlen(s);
int id=0,maxlen=0;
k=0;
ch[k++] = '*';
for(int i=0;i<len;i++){
ch[k++] = '#';
ch[k++] = s[i];
if(i==len-1)
ch[k++] = '#';
}
ch[k++] = '\0';
// printf("%s\n",ch);
memset(p,0,sizeof p);
for(int i=2;i<2*len+1;i++){
if(p[id]+id>i)
p[i] = Min(p[2*id-i],p[id]+id-i);
else
p[i] = 1;
while(ch[i-p[i]] == ch[i+p[i]])
p[i]++;
if(p[id]+id<i+p[i])
id = i;
if(maxlen<p[i])
maxlen = p[i];
}
printf("%d\n",maxlen-1);
}
刚才说了,算法是基于有个中心点的,所以偶数长度的无法处理,我们的做法是可以再每个字符中心加个没有出现过的符号,如'#',这样,所有的回文串的长度就都是奇数了。
p[id]刚才说了,是以第id个字符为中心的最长回文串的半径,那么如果现在循环到了第i个字符,那么如果在id<i中没有半径覆盖到i(即p[id]+id<=i),那么此时我们已知的最长回文子串的长度为1,剩下的就是向两边扩展比较了。
上面的是第一大情况,对于第二大情况,如果有p[id]+id>i,说明第i个字符之前已经在某个回文串之内了。
这种情况又分为3种情况。
这就是第一种情况,图中的i就是我们说的id,i+kj就是我们说的i,那么因为i+k是在以i为中心回文串种,那么与i+k对应的就是i-k那部分,如果i-k回文串有一部分在i的半径外,那么其实是不可能的,因为如果这样,那么i的半径就随之延伸了。所以此时p[i+k] = p[i]+i-k (对应到代码中就是p[i]=p[id]+id-i)。
这是第二种情况,就是i-k-[i-k]在以i为半径之内,说明我们现在遍历到的i+k最长的回文串长度也是在p[i]半径之内的,所以此时p[i+k] = p[i-k](在代码中就是p[id-(i-id))]=p[2*id-i];
第三种情况的p[i+k]值是和第二种情况是一样的,但是在这种情况下p[i+k]的值还可能增加,因为再往外延伸时,我们就不知道了,而第二种情况下p[i+k]=p[i-k],不可能再增加了。
上面的两大情况和3种小情况合起来就能得出以上的核心代码。