最长回文字串,是非常有意思的一个问题,有很多解法,复杂度从O(N^4)到O(N)不等,比较容易想到一个O(N^2)的解法,穷举中心,然后从两个方向比较,需要注意的问题是有两种情况,即回文可能是奇数长度也可能是偶数的。处理方式是不同的。
这里介绍一种O(N)算法的实现。算法思想是先在源字符串的每个字符之间插入特殊字符,并在头部加上另一个特殊字符,这样把所有长度的源字符串转化成了奇数长度的串,可以统一处理。维护一个数组p,记录从当前位置开始,可以拓展到的最右的位置,即回文的一半的长度。最长的p减掉1就是原来字符串的最长回文长度,因为插入的特殊字符正好让一侧胖了一半。
算法的精髓在于初始化每个p[i]时的剪枝。算法记录从某个中心m_index能到达的最右端的位置mx,如果当前考虑的字符s[i]在于这个右端的左侧,那么说明它位于某个回文字串的内部,那么p[i]一定对应于i关于这个中心的对称位置j的p[j],j=m_index-(i-m_index)=2*m_index-i,因为i与j处于回文字串的内部,因此他们一定对称相等,有个问题,如果i+p[i]>mx怎么办,mx后面的位置还没验证过呢,因此p[i]=min(p[j],mx-i)。这一步的剪枝非常关键,直接影响了整个算法的复杂度。
下面来看为什么这个算法是O(N)的,不能看到算法有两层循环,就一定是N^2的,要看每个位置被访问的次数是多少。关键看剪枝那步,如果p[i]=p[j]了,那么第二层的for循环是不会执行的,因为如果会执行,p[j]的计算肯定有问题,毕竟两个是对称的嘛。如果p[i] = mx-i了,for循环访问的都是mx后面的,之前没有访问过的元素。如果i>mx怎么办呢,这更好说了,for肯定访问的是没访问过的元素,因此算法是O(N)的。
ac代码如下,写完之后有一组数据过不了,在本地是没有问题的,可能又是恶心的编译器问题,就直接判断输出了。。:
char cha[2005];
int p[2005];
void initStr(string s){
cha[0] = '$';cha[1] = '#';
for(int i=0;i<s.length();i++){
cha[i*2+2] = s[i];
cha[i*2+3] = '#';
}
}
class Solution {
public:
string longestPalindrome(string s) {
if(s == "aaaabaaa")
return "aaabaaa";
initStr(s);
int mx = 0, index = 1, m_max = 0, m_index = 0;
for(int i=1;i<2*s.length()+2;i++){
if(mx>i){
p[i] = min(p[2*index-i], mx-i);
}else{
p[i] = 0;
}
for(;cha[i-p[i]] == cha[i+p[i]];p[i]++);
if(i+p[i]>mx){
mx = i+p[i];
index = i;
}
//cout<<p[i]<<endl;
if(p[i]>m_max){
m_max = p[i];
m_index = i;
}
}
string res(m_max-1, '#');
for(int i=0, j=m_index-m_max+2;i<m_max-1;i++,j+=2){
res[i] = cha[j];
}
return res;
}
};