leetcode刷题题解之最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad" 输出: "bab" 注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd" 输出: "bb"
📺视频题解
📖文字题解
方法一:动态规划
思路与算法
对于一个子串而言,如果它是回文串,并且长度大于 ,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 ,如果我们已经知道 是回文串,那么 一定是回文串,这是因为它的首尾两个字母都是 。
根据这样的思路,我们就可以用动态规划的方法解决本题。我们用 表示字符串 的第 到 个字母组成的串(下文表示成 )是否为回文串:
这里的「其它情况」包含两种可能性:
-
本身不是一个回文串;
-
,此时 本身不合法。
那么我们就可以写出动态规划的状态转移方程:
也就是说,只有 是回文串,并且 的第 和 个字母相同时, 才会是回文串。
上文的所有讨论是建立在子串长度大于 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 或 。对于长度为 的子串,它显然是个回文串;对于长度为 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
根据这个思路,我们就可以完成动态规划了,最终的答案即为所有 中 (即子串长度)的最大值。注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)]
ans = ""
# 枚举子串的长度 l+1
for l in range(n):
# 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置
for i in range(n):
j = i + l
if j >= len(s):
break
if l == 0:
dp[i][j] = True
elif l == 1:
dp[i][j] = (s[i] == s[j])
else:
dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
if dp[i][j] and l + 1 > len(ans):
ans = s[i:j+1]
return ans
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
string ans;
for (int l = 0; l < n; ++l) {
for (int i = 0; i + l < n; ++i) {
int j = i + l;
if (l == 0) {
dp[i][j] = 1;
}
else if (l == 1) {
dp[i][j] = (s[i] == s[j]);
}
else {
dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]);
}
if (dp[i][j] && l + 1 > ans.size()) {
ans = s.substr(i, l + 1);
}
}
}
return ans;
}
};
func longestPalindrome(s string) string {
n := len(s)
ans := ""
dp := make([][]int, n)
for i := 0; i < n; i++ {
dp[i] = make([]int, n)
}
for l := 0; l < n; l++ {
for i := 0; i + l < n; i++ {
j := i + l
if l == 0 {
dp[i][j] = 1
} else if l == 1 {
if s[i] == s[j] {
dp[i][j] = 1
}
} else {
if s[i] == s[j] {
dp[i][j] = dp[i+1][j-1]
}
}
if dp[i][j] > 0 && l + 1 > len(ans) {
ans = s[i:i+l+1]
}
}
}
return ans
}
复杂度分析
-
时间复杂度:,其中 是字符串的长度。动态规划的状态总数为 ,对于每个状态,我们需要转移的时间为 。
-
空间复杂度:,即存储动态规划状态需要的空间。
方法二:中心扩展算法
思路与算法
我们仔细观察一下方法一中的状态转移方程:
找出其中的状态转移链:
可以发现,所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。
边界情况即为子串长度为 或 的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。如果两边的字母相同,我们就可以继续扩展,例如从 扩展到 ;如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。
聪明的读者此时应该可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。方法二的本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。
class Solution {
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);
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> <span class="hljs-title">expandAroundCenter</span><span class="hljs-params">(String s, <span class="hljs-keyword">int</span> left, <span class="hljs-keyword">int</span> right)</span> </span>{
<span class="hljs-keyword">int</span> L = left, R = right;
<span class="hljs-keyword">while</span> (L >= <span class="hljs-number">0</span> && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
<span class="hljs-keyword">return</span> R - L - <span class="hljs-number">1</span>;
}
}
class Solution:
def expandAroundCenter(self, s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return left + 1, right - 1
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">longestPalindrome</span><span class="hljs-params">(self, s: str)</span> -> str:</span>
start, end = <span class="hljs-number">0</span>, <span class="hljs-number">0</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(s)):
left1, right1 = self.expandAroundCenter(s, i, i)
left2, right2 = self.expandAroundCenter(s, i, i + <span class="hljs-number">1</span>)
<span class="hljs-keyword">if</span> right1 - left1 > end - start:
start, end = left1, right1
<span class="hljs-keyword">if</span> right2 - left2 > end - start:
start, end = left2, right2
<span class="hljs-keyword">return</span> s[start: end + <span class="hljs-number">1</span>]
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {left + 1, right - 1};
}
<span class="hljs-function"><span class="hljs-built_in">string</span> <span class="hljs-title">longestPalindrome</span><span class="hljs-params">(<span class="hljs-built_in">string</span> s)</span> </span>{
<span class="hljs-keyword">int</span> start = <span class="hljs-number">0</span>, end = <span class="hljs-number">0</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < s.size(); ++i) {
<span class="hljs-keyword">auto</span> [left1, right1] = expandAroundCenter(s, i, i);
<span class="hljs-keyword">auto</span> [left2, right2] = expandAroundCenter(s, i, i + <span class="hljs-number">1</span>);
<span class="hljs-keyword">if</span> (right1 - left1 > end - start) {
start = left1;
end = right1;
}
<span class="hljs-keyword">if</span> (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
<span class="hljs-keyword">return</span> s.substr(start, end - start + <span class="hljs-number">1</span>);
}
};
func longestPalindrome(s string) string {
if s == "" {
return ""
}
start, end := 0, 0
for i := 0; i < len(s); i++ {
left1, right1 := expandAroundCenter(s, i, i)
left2, right2 := expandAroundCenter(s, i, i + 1)
if right1 - left1 > end - start {
start, end = left1, right1
}
if right2 - left2 > end - start {
start, end = left2, right2
}
}
return s[start:end+1]
}
func expandAroundCenter(s string, left, right int) (int, int) {
for ; left >= 0 && right < len(s) && s[left] == s[right]; left, right = left-1 , right+1 { }
return left + 1, right - 1
}
复杂度分析
-
时间复杂度:,其中 是字符串的长度。长度为 和 的回文中心分别有 和 个,每个回文中心最多会向外扩展 次。
-
空间复杂度:。
方法三:Manacher 算法
还有一个复杂度为 的 Manacher 算法。然而本算法十分复杂,一般不作为面试内容。这里给出,仅供有兴趣的同学挑战自己。
为了表述方便,我们定义一个新概念臂长,表示中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * length + 1 ,其臂长为 length。
下面的讨论只涉及长度为奇数的回文字符串。长度为偶数的回文字符串我们将会在最后与长度为奇数的情况统一起来。
思路与算法
在中心扩展算法的过程中,我们能够得出每个位置的臂长。那么当我们要得出以下一个位置 i 的臂长时,能不能利用之前得到的信息呢?
答案是肯定的。具体来说,如果位置 j 的臂长为 length,并且有 j + length > i,如下图所示:

当在位置 i 开始进行中心拓展时,我们可以先找到 i 关于 j 的对称点 2 * j - i。那么如果点 2 * j - i 的臂长等于 n,我们就可以知道,点 i 的臂长至少为 min(j + length - i, n)。那么我们就可以直接跳过 i 到 i + min(j + length - i, n) 这部分,从 i + min(j + length - i, n) + 1 开始拓展。
我们只需要在中心扩展法的过程中记录右臂在最右边的回文字符串,将其中心作为 j,在计算过程中就能最大限度地避免重复计算。
那么现在还有一个问题:如何处理长度为偶数的回文字符串呢?
我们可以通过一个特别的操作将奇偶数的情况统一起来:我们向字符串的头尾以及每两个字符中间添加一个特殊字符 #,比如字符串 aaba 处理后会变成 #a#a#b#a#。那么原先长度为偶数的回文字符串 aa 会变成长度为奇数的回文字符串 #a#a#,而长度为奇数的回文字符串 aba 会变成长度仍然为奇数的回文字符串 #a#b#a#,我们就不需要再考虑长度为偶数的回文字符串了。
注意这里的特殊字符不需要是没有出现过的字母,我们可以使用任何一个字符来作为这个特殊字符。这是因为,当我们只考虑长度为奇数的回文字符串时,每次我们比较的两个字符奇偶性一定是相同的,所以原来字符串中的字符不会与插入的特殊字符互相比较,不会因此产生问题。
class Solution:
def expand(self, s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return (right - left - 2) // 2
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">longestPalindrome</span><span class="hljs-params">(self, s: str)</span> -> str:</span>
end, start = <span class="hljs-number">-1</span>, <span class="hljs-number">0</span>
s = <span class="hljs-string">'#'</span> + <span class="hljs-string">'#'</span>.join(list(s)) + <span class="hljs-string">'#'</span>
arm_len = []
right = <span class="hljs-number">-1</span>
j = <span class="hljs-number">-1</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(s)):
<span class="hljs-keyword">if</span> right >= i:
i_sym = <span class="hljs-number">2</span> * j - i
min_arm_len = min(arm_len[i_sym], right - i)
cur_arm_len = self.expand(s, i - min_arm_len, i + min_arm_len)
<span class="hljs-keyword">else</span>:
cur_arm_len = self.expand(s, i, i)
arm_len.append(cur_arm_len)
<span class="hljs-keyword">if</span> i + cur_arm_len > right:
j = i
right = i + cur_arm_len
<span class="hljs-keyword">if</span> <span class="hljs-number">2</span> * cur_arm_len + <span class="hljs-number">1</span> > end - start:
start = i - cur_arm_len
end = i + cur_arm_len
<span class="hljs-keyword">return</span> s[start+<span class="hljs-number">1</span>:end+<span class="hljs-number">1</span>:<span class="hljs-number">2</span>]
class Solution {
public:
int expand(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return (right - left - 2) / 2;
}
<span class="hljs-function"><span class="hljs-built_in">string</span> <span class="hljs-title">longestPalindrome</span><span class="hljs-params">(<span class="hljs-built_in">string</span> s)</span> </span>{
<span class="hljs-keyword">int</span> start = <span class="hljs-number">0</span>, end = <span class="hljs-number">-1</span>;
<span class="hljs-built_in">string</span> t = <span class="hljs-string">"#"</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">char</span> c: s) {
t += c;
t += <span class="hljs-string">'#'</span>;
}
t += <span class="hljs-string">'#'</span>;
s = t;
<span class="hljs-built_in">vector</span><<span class="hljs-keyword">int</span>> arm_len;
<span class="hljs-keyword">int</span> right = <span class="hljs-number">-1</span>, j = <span class="hljs-number">-1</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < s.size(); ++i) {
<span class="hljs-keyword">int</span> cur_arm_len;
<span class="hljs-keyword">if</span> (right >= i) {
<span class="hljs-keyword">int</span> i_sym = j * <span class="hljs-number">2</span> - i;
<span class="hljs-keyword">int</span> min_arm_len = min(arm_len[i_sym], right - i);
cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len);
}
<span class="hljs-keyword">else</span> {
cur_arm_len = expand(s, i, i);
}
arm_len.push_back(cur_arm_len);
<span class="hljs-keyword">if</span> (i + cur_arm_len > right) {
j = i;
right = i + cur_arm_len;
}
<span class="hljs-keyword">if</span> (cur_arm_len * <span class="hljs-number">2</span> + <span class="hljs-number">1</span> > end - start) {
start = i - cur_arm_len;
end = i + cur_arm_len;
}
}
<span class="hljs-built_in">string</span> ans;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = start; i <= end; ++i) {
<span class="hljs-keyword">if</span> (s[i] != <span class="hljs-string">'#'</span>) {
ans += s[i];
}
}
<span class="hljs-keyword">return</span> ans;
}
};
func longestPalindrome(s string) string {
start, end := 0, -1
t := "#"
for i := 0; i < len(s); i++ {
t += string(s[i]) + "#"
}
t += "#"
s = t
arm_len := []int{}
right, j := -1, -1
for i := 0; i < len(s); i++ {
var cur_arm_len int
if right >= i {
i_sym := j * 2 - i
min_arm_len := min(arm_len[i_sym], right-i)
cur_arm_len = expand(s, i-min_arm_len, i+min_arm_len)
} else {
cur_arm_len = expand(s, i, i)
}
arm_len = append(arm_len, cur_arm_len)
if i + cur_arm_len > right {
j = i
right = i + cur_arm_len
}
if cur_arm_len * 2 + 1 > end - start {
start = i - cur_arm_len
end = i + cur_arm_len
}
}
ans := ""
for i := start; i <= end; i++ {
if s[i] != '#' {
ans += string(s[i])
}
}
return ans
}
func expand(s string, left, right int) int {
for ; left >= 0 && right < len(s) && s[left] == s[right]; left, right = left-1, right+1 { }
return (right - left - 2) / 2
}
func min(x, y int) int {
if x < y {
return x
}
return y
}
复杂度分析
-
时间复杂度:,其中 是字符串的长度。由于对于每个位置,扩展要么从当前的最右侧臂长
right开始,要么只会进行一步,而right最多向前走 步,因此算法的复杂度为 。 -
空间复杂度:,我们需要 的空间记录每个位置的臂长。


浙公网安备 33010602011771号