最长回文子串,leetcode第5题。
最简单的做法就是暴力破解,遍历每一个可能的字符做中心,查看可能的备选回文子串,但是时间复杂度为O(n^2)。
利用动态规划,可以省去比较的过程,但是时间复杂度没有量级提升。用动态规划时踩了坑:在用动态规划解决这个问题的时候,笔者没忽略了一个细节:当子串长度小于等于3的时候,只要当前比较字符相同则不用比较内部是否相同。这个细节的忽略导致常见的二八定律,20%搞定了80%的问题,剩下越到中心的节点则分了好几种情况讨论,写了80%的代码。现在想想着实惭愧。。。。
马拉车算法(Manacher’s Algorithm)是由Manacher 1972年提出,不得不佩服这个人,因为这个算法思想其实普通大众能很直观的感受到,但是把它用详细的步骤描述出来确实厉害。算法利用已得到回文来计算以后续字符串为中心的回文。
以下为具体步骤:
第一步用一个小trick,用#间隔开原始字符串的各个字符,这样就不用考虑原始字符串长度的奇偶问题,经过改进的字符串长度为奇数。
那么原始的字符串为:abcbcaad,加入#后改进为:#a#b#c#b#c#a#a#d#,接着做以下标记
i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
arr[i] # a # b # c # b # c # a # a # d #
r[i] 1 2 1 2 1 4 1 2 1 2 1 2 3 2 1 2 1
第二步利用以上信息获取原始回文长度和起始位置
1.回文长度为r[i]-1。
为什么是r[i] -1? 假设回文长度为L, 那么加入trick后的会问总长度为2L+1, 而r[i]=L+1,所以L=r[i] - 1
2.回文的起始位置为(i-L)/2 ,注意此处不能是i/2-L/2,反例为源字符串是“a”
第三步如何计算p[i]
这个是算法的核心,比如当知道i=5时,r[1]=5. 那么r[i+1] = r[i-1],记j=i-1,max_right=i+r[i]这个有个前提就是j<max_right && (i-1)-r[i-1] > i - r[i].所以接下来的重点就是如何初始化r[j]。 在下面的代码中新的构造的字符串的开头和结尾加了^%,主要是为了防止下标越界
具体代码:
class Solution {
public:
string longestPalindrome(string s) {
string new_s = "^#"; // 预处理之后的string
for(int i=0; i<s.size(); i++){
new_s += s[i];
new_s += "#";
}
new_s += "%";
int new_length = new_s.size();
int radius[new_length];
int center = 1;
int max_right = 1;
int max_length = -1;
int max_center = 1; // 回文长度最长时,对应的中心
// 计算new_s的每个字符的半径radius
for(int i=1; i<new_length; i++){
radius[i] = max_right > i? min(radius[2*center - i], max_right-i): 1;
while(new_s[i-radius[i]] == new_s[i+radius[i]]){ //如果new_s不加^%会下标地址越界
radius[i] ++;
}
if(radius[i]+i > max_right){
max_right = radius[i]+i;
center = i;
}
if(radius[i]-1 > max_length){
max_length = radius[i]-1;
max_center = i;
}
}
int start = (max_center - max_length)/2;
string ret_s = s.substr(start, max_length);
return ret_s;
}
};