题目描述
Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.
即给定一个字符串,求它的最长回文子串的长度(或者最长回文子串)。
解法一
对于一个问题,一定可以找到一个傻的可爱的暴力解法,本题的暴力解法即:遍历整个字符串,以每一个字符为中心寻找以该字符为中心的最长回文子串,一次遍历下来即可获取最长回文子串,但是相应的这种方法的复杂度很糟糕O(N2)
这里值得注意的是要分开处理字符串长度为奇数或者偶数的情况
代码如下:
public String longestPalindrome(String s) {
String max = "";
int len = s.length();
for (int i = 0; i < len; i++) {
int odd = 0;
for (int j = 1; i + j < len && i >= j; j++) {
if (s.charAt(i + j) == s.charAt(i - j))
++odd;
else
break;
}
if (2 * odd + 1 > max.length())
max = s.substring(i - odd, i + odd + 1);
int even = 0;
for (int j = 1; i + j + 1 < len && i >= j; j++) {
if (s.charAt(i + j + 1) == s.charAt(i - j))
++even;
else
break;
}
if (even * 2 + 2 > max.length())
max = s.substring(i - even, i + even + 2 > s.length() ? s.length() : i + even + 2);
} return max;
}
解法二
OK,笨的方法找到了,怎么样去提高效率。观察上面遍历的过程,对于字符串S,当以第i个字符为中心时,我们需要重新计算其最长回文子串,每个都是重头开始计算,那么能否利用回文字符串的性质来减少这种计算量从而提高整个算法的效率?答案是肯定的,下面要说的Manacher's 算法就是这样
统一奇偶
在正式开始算法之前,能否有一个方法将奇偶两种情况统一起来?为了达到这个目的,我们可以将原字符串S每个字符之间插入一个特殊字符'#',得到一个新的字符串T,如下
- S = "abaaba", T = "#a#b#a#a#b#a#".
这样可以发现无论长度奇偶都转换为奇情况来处理
Manacher算法
开头提到的思想,当我们要找寻以Tj为中心的回文子串时,能否利用前面已经算出的以Ti(i<j)为中心的回文子串,所以我们将前面算出的中间结果存入数组P,P[i]表示已Ti为中心的回文子串的长度(不包括其自身),而最长回文子串的长度就是P中的最大值。我们接着上面的例子
我们通过字符串T将对应的P写出,由于P6 = 6,可以很容易得出最长回文子串是"abaaba"
那么我们现在的关注点就主要放在如何计算P上,当然利用解法一那样我们可以通过遍历每一个点作为中心点来获取P,但是效率很低。还是前面提到的那个思想,当计算到第i个点作为中心点时,能否利用前面已经计算过的点。
观察回文串"abaaba"所对应的P,我们可以发现一条有用的规律,以i = 6作为中心,P中的数据时关于这个中心对称的,这不是偶然,我们可以尝试"aba",我们可以发现相似的对称性质。如果这条性质可以使用,我们就可以减少重复计算P的值。
为了验证这个思想,我们举一个稍微复杂一些的例子,S = "babcbabcbaccba",我们以图展现部分计算P的过程,从中找寻规律:
假设我们已经计算出一部分P的值,图中实线表示回文子串"abcbabcba"的中心位置,而虚线则表示该子串的左右边界
现在当我们想要计算i=13时P的值,已知i关于C的对称点i',我们怎样快速求出P[ i ]?
图中给出了我们的算法进行到i = 13的时候,我们需要计算P[ 13 ],我们来观察i关于回文子串中心点C的对称点i‘ = 9
图中我们用绿线标出了分别以i'和i为中心的回文子串,可以发现由于关于C点的对称性质,很容易得出P[ i ] = P[ i' ] = 1
通过以上分析,由于关于C的对称性质,我们很开心的得出一个结论P[ i ] = P[ i' ] = 1,而且之后的三个元素也都可以利用这条性质得出P的值(P[ 12 ] = P[ 10 ] = 0, P[ 13 ] = P[ 9 ] = 1, P[ 14 ] = P[ 8 ] = 0)
现在我们需要计算P[ 15 ],而i = 15关于C的对称点是i' = 7,那么p[ 15 ] = P[ 7 ] = 7?
当我们需要计算i = 15的时候,我们利用上面总结出的规律可以得出P[ 15 ] = P[ 7 ] = 7,但是进一步去计算P[ 15 ] 我们会发现以i = 15为中心点的最长回文子串是"a#b#c#b#a",事实是P[ 15 ] 要比对称点的子串短,这是为什么呢?
图中我们将以i'和以i为中心的子串用线标出,其中以绿线标出了严格根据中心点C对称的部分,而红线标出了超出
以C为中心点的子串的左右边界的部分,绿色的虚线部分标出了跨过中心点的部分
我们可以很清晰的发现,两个子串在绿色线标出的地方是完全对称的,同样绿色虚线部分由于中心对称也是满足的。然而P[ i' ] = 7,所以以i'为中心的子串超出了左边界,则这部分不再满足对称性质。我们只是知道P[ i ] ≥ 5,即i到右边界的长度。为了进一步得出P[ i ] 的值,我们需要向右扩展,在这里,由于P[ 21 ] ≠ P[ 7 ],所以P[ i ] = 5
我们总结一下上面发现的规律:
如果P[ i' ] ≤ R - i
那么 P[ i ] = P[ i' ]
否则 P[ i ] ≥ R - i (之后我们需要扩展出右边界来找到最终的P[ i ])
另外我们需要判断一下什么时候移动中心点C以及其右边界R
当以i为中心点的回文子串超出了右边界R时,我们将C移至i,将右边界R移至i的右边界
AC代码如下
private String insert(String s){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
sb.append('#');
sb.append(s.charAt(i));
}
sb.append('#'); return sb.toString();
} public String longestPalindrome(String s) {
String t = insert(s);
int[] p = new int[t.length()];
int c = 0, r = 0; // 当前中心位置以及当前中心位置的右边界
for (int i = 0; i < t.length(); i++) {
int i_mirror = 2 * c - i;
p[i] = r > i ? Math.min(r - i, p[i_mirror]) : 0;
while (i + p[i] + 1 < t.length() && i - p[i] - 1 >= 0
&& t.charAt(i + p[i] + 1) == t.charAt(i - p[i] - 1))
p[i]++;
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
} int maxC = 0;
for (int i = 0; i < t.length(); i++) {
if (p[i] > p[maxC])
maxC = i;
}
return t.substring(maxC - p[maxC], maxC + p[maxC]).replace("#", "");
}
由于我们有两个变量中心点C以及右边界R,当P[ i ] ≤ R – i,我们直接以O(1)进行计算,而另一种情况则需要移动中心点和右边界,最多两者都是移动N步,所以总的时间负责度时O(2*N)即O(N)
——reference