LeetCode有一道最长回文子串的题,下面基本是官方解答的翻译版。
一些人可能会快速想到一个解决方法,然而这个方法却是错误的(但是很容易将它修改成正确的):
将 S S S 反转成 S ′ S^\prime S′,然后找到 S S S 和 S ′ S^\prime S′ 的最长公共子串,这个最长公共子串就是 S S S 的最长回文子串。
这看起来是没有问题的,比如 S = c a b a S = caba S=caba 和 S ′ = a b a c S^\prime = abac S′=abac 的最长公共子串 aba 就是正确答案。
但是对于 S = a b a c d f g d c a b a S = abacdfgdcaba S=abacdfgdcaba 和 S ′ = a b a c d g f d c a b a S^\prime = abacdgfdcaba S′=abacdgfdcaba 的最长公共子串 abacd 就显然不是一个回文子串。
我们可以看到,当 S S S 的某一个非回文子串正好是另一子串的反转,最长公共子串方法就会失败。为了纠正这个问题,每次我们找到一个最长的公共子串候选者时,我们检查子串的索引是否与被反转子串的原始索引相同。如果是,那么我们尝试更新到目前为止发现的最长的回文;如果不是,我们就跳过这个,找到下一个候选子串。
这给了我们一个时间复杂度 O ( n 2 ) O(n^2) O(n2) ,空间复杂度 O ( n 2 ) O(n^2) O(n2) (空间复杂度可以改进到 O ( n ) O(n) O(n))的动态规划解决方案。更多信息参考:
维基百科:Longest common substring problem 和我的博客:字符串系列3 最长公共子串。
列举所有子串,然后查看子串是否是回文串,不说了。
时间复杂度为 O ( n 3 ) O(n^3) O(n3),空间复杂度为 O ( 1 ) O(1) O(1),空间复杂度可进一步改进。
为了改进暴力搜索,我们首先观察如何在验证回文时避免不必要的重新计算。以“ababa”为例。如果我们已经知道bab是一个回文,很明显ababa必须是回文,因为两个左右两边的字母是相同的。
我们将 P ( i , j ) P(i,j) P(i,j) 定义为:
P ( i , j ) = { t r u e , i f t h e s u b s t r i n g S i . . . S j i s a p a l i n d r o m e f a l s e , o t h e r w i s e . P(i,j)=\left\{ \begin{aligned} &true, & if\ the\ substring\ S_i\ ...\ S_j is\ a\ palindrome\\ &false, & otherwise. \end{aligned} \right. P(i,j)={true,false,if the substring Si ... Sjis a palindromeotherwise.
因此, P ( i , j ) = ( P ( i + 1 , j − 1 ) a n d S i = = S j ) P(i,j)=(P(i+1,j-1)\ and \ S_i ==S_j) P(i,j)=(P(i+1,j−1) and Si==Sj)
初始条件为: P ( i , i ) = t r u e P(i,i)=true P(i,i)=true P ( i , i + 1 ) = ( S i = = S i + 1 ) P(i,i+1)=(S_i==S_{i+1}) P(i,i+1)=(Si==Si+1) 然后我们就可以使用动态规划解决问题,我们首先初始化一个和两个字母的回文,然后逐步找到所有三个字母回文,以此类推下去。
DP的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度可进一步改进。
事实上,我们可以用 O ( n 2 ) O(n^2) O(n2)的时间复杂度和 O ( 1 ) O(1) O(1) 的空间复杂度解决问题。
回文串是中心对称的,因此,回文可以从其中心扩展,并且只有 2 n − 1 2n-1 2n−1 个这样的中心。
你可能会问为什么有 2 n − 1 2n-1 2n−1 个而不是 n n n 个中心?原因是回文的中心不止可以在字母上,也可以在两个字母之间,这些回文具有偶数个字母,例如abba,其中心位于两个b之间,Java 代码如下:
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
这个算法在我之前的博客里面写过,附上链接:字符串系列2 Manacher 算法