给你一个字符串
s
,找到s
中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
时间复杂度:O(n2)
空间复杂度:O(n2)
对于一个子串而言,如果它是回文串,并且长度大于
2
,那么将它首尾的两个字母去除之后,它仍然是个回文串。
例如对于字符串“ababa”
,如果我们已经知道“bab”
是回文串,那么“ababa”
一定是回文串,这是因为它的首尾两个字母都是“a”
。
根据这样的思路,我们就可以用动态规划的方法解决本题。
我们用P(i,j)
表示字符串s
的第i
到j
个字母组成的串(下文表示成s[i:j]
)是否为回文串:
P ( i , j ) = { t r u e ,如果说子串 S i . . . S j 是回文串 f a u l s e ,其他情况 P(i,j)=\left\{ \begin{aligned} true & ,如果说子串S~i~...S~j~是回文串\\ faul&se ,其他情况 \end{aligned} \right. P(i,j)={truefaul,如果说子串S i ...S j 是回文串se,其他情况
这里的「其它情况」包含两种可能性:
s[i,j]
本身不是一个回文串;
i>j
,此时s[i,j]
本身不合法。
那么我们就可以写出动态规划的状态转移方程:
P ( i , j ) = P ( i + 1 , j − 1 ) Λ ( S i = = S j ) P(i,j)=P(i+1,j-1)\Lambda(S~i~==S~j~) P(i,j)=P(i+1,j−1)Λ(S i ==S j )
也就是说,只有
s[i+1:j−1]
是回文串,并且s
的第i
和j
个字母相同时,s[i:j]
才会是回文串。
上文的所有讨论是建立在子串长度大于
2
的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为1
或2
。
对于长度为1
的子串,它显然是个回文串;对于长度为2
的子串,只要它的两个字母相同,它就是一个回文串。
因此我们就可以写出动态规划的边界条件:
{ P ( i , j ) = t r u e P ( i , i + 1 ) = ( S i = = S i + 1 ) \left\{ \begin{aligned} P(i,j)=true\\ P(i,i+1)=(&S~i~==S~i+1~) \end{aligned} \right. {P(i,j)=trueP(i,i+1)=(S i ==S i+1 )
根据这个思路,我们就可以完成动态规划了,最终的答案即为所有
P(i,j)=true
中j−i+1
(即子串长度)的最大值。
注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
#include
#include
#include
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size(); // 字符串长度
if (n < 2) { // 其长度小于 2,则其最大回文子串为其本身
return s;
}
int maxLen = 1; // 记录最大回文子串长度,初始值为 1
int begin = 0; // 回文子串起始位置,初始为 0
// dp[i][j] 表示 s[i..j] 是否是回文串
vector> dp(n, vector(n));
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= n; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < n; i++) { // i,j 为左右边界
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= n) {
break;
}
if (s[i] != s[j]) { // S i ≠ S j,S[i,j] 一定不是回文子串
dp[i][j] = false;
} else { // S i = S j
if (j - i < 3) { // S[i,j] 的长度 ≤ 2,则 S[i,j] 一定是回文子串
dp[i][j] = true;
} else { // S[i,j] 的长度 > 2,此时看 S[i+1,j-1] 是否为回文子串
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substr(begin, maxLen); // 返回最长回文子串
}
};
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s) # 字符串长度
if n < 2: # 其长度小于 2,则其最大回文子串为其本身
return s
max_len = 1 # 记录最大回文子串长度,初始值为 1
begin = 0 # 回文子串起始位置,初始为 0
# dp[i][j] 表示 s[i..j] 是否是回文串
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True
# 递推开始
# 先枚举子串长度
for L in range(2, n + 1):
# 枚举左边界,左边界的上限设置可以宽松一些
for i in range(n): # i,j 为左右边界
# 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
j = L + i - 1
# 如果右边界越界,就可以退出当前循环
if j >= n:
break
if s[i] != s[j]: # S i ≠ S j,S[i,j] 一定不是回文子串
dp[i][j] = False
else:
if j - i < 3: # S[i,j] 的长度 ≤ 2,则 S[i,j] 一定是回文子串
dp[i][j] = True
else: # S[i,j] 的长度 > 2,此时看 S[i+1,j-1] 是否为回文子串
dp[i][j] = dp[i + 1][j - 1]
# 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if dp[i][j] and j - i + 1 > max_len:
max_len = j - i + 1
begin = i
return s[begin:begin + max_len] # 返回最长回文子串
public class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
boolean[][] dp = new boolean[len][len];
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] charArray = s.toCharArray();
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= len; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < len; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= len) {
break;
}
if (charArray[i] != charArray[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
时间复杂度:O(n2)
空间复杂度:O(1)
我们仔细观察一下方法一中的状态转移方程:
{ P ( i , i ) = t r u e P ( i , i + 1 ) = ( S i = = S i + 1 ) P ( i , j ) = P ( i + 1 , j − 1 ) Λ ( S i = = S j ) \left\{ \begin{aligned} P(i,i)=true\\ P(i,i+1)=(&S~i~==S~i+1~)\\ P(i,j)=P(i+&1,j-1)\Lambda(S~i~==S~j~) \end{aligned} \right. ⎩ ⎨ ⎧P(i,i)=trueP(i,i+1)=(P(i,j)=P(i+S i ==S i+1 )1,j−1)Λ(S i ==S j )
找出其中的状态转移链:
P ( i , j ) ← P ( i + 1 , j − 1 ) ← P ( i + 2 , j − 2 ) ← ⋯ ← 某一边界情况 P(i,j)←P(i+1,j−1)←P(i+2,j−2)←⋯←某一边界情况 P(i,j)←P(i+1,j−1)←P(i+2,j−2)←⋯←某一边界情况
可以发现,所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。
边界情况即为子串长度为
1
或2
的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。
如果两边的字母相同,我们就可以继续扩展,例如从P(i+1,j−1)
扩展到P(i,j)
;如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。
聪明的读者此时应该可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。
方法二的本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。
我们对所有的长度求出最大值,即可得到最终的答案。
class Solution {
public:
// 回文串中心扩展函数
pair 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}; // 返回子串在原串的左右边界
}
// 求出最大子串长度并返回子串
string longestPalindrome(string s) {
int start = 0, end = 0; // 最大子串起始位置,初始为 0
for (int i = 0; i < s.size(); ++i) {
auto [left1, right1] = expandAroundCenter(s, i, i); // 单字符串中心扩展
auto [left2, right2] = expandAroundCenter(s, i, i + 1); // 双字符串中心扩展
// 若找到新的最大子串,更新起始位置
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1); // 返回最大子串
}
};
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 # 返回子串在原串的左右边界
# 求出最大子串长度并返回子串
def longestPalindrome(self, s: str) -> str:
start, end = 0, 0 # 最大子串起始位置,初始为 0
for i in range(len(s)):
left1, right1 = self.expandAroundCenter(s, i, i) # 单字符串中心扩展
left2, right2 = self.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] # 返回最大子串
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);
}
public int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return right - left - 1;
}
}
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
}
时间复杂度:O(n)
空间复杂度:O(n)
还有一个复杂度为
O(n)
的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 {
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; // 返回臂长,此臂长不包括回文串中心点
}
string longestPalindrome(string s) {
int start = 0, end = -1; // 回文子串的起始位置
// 插入特殊字符
string t = "#";
for (char c: s) {
t += c;
t += '#';
}
t += '#';
s = t;
vector arm_len; // 记录已经求好的回文子串的臂长
int right = -1, j = -1; // right 为右边界,j 为当前最大回文子串的中心点
for (int i = 0; i < s.size(); ++i) {
int cur_arm_len; // 记录当前的回文子串臂长
if (right >= i) { // i 在当前最大回文子串的臂长的右边界内
int i_sym = j * 2 - i; // i 关于 j 的对称点
int min_arm_len = min(arm_len[i_sym], right - i); // i 的最小臂长
cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len); // 跳过 i 的最小臂长进行中心扩展
} else { // i 已经超过了右边界,此时 i 与前面无任何关系,直接暴力扩展
cur_arm_len = expand(s, i, i); // 直接在 i 的当前位置进行中心扩展
}
arm_len.push_back(cur_arm_len); // 将得到的臂长加入到容器 arm_len 的尾部
if (i + cur_arm_len > right) { // 当前 i 加上其臂长已经超过右边界了,更新右边界以及新的中心点
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;
}
}
string ans; // 将上面得到的回文串剔除特殊字符后放入 ans 中
for (int i = start; i <= end; ++i) {
if (s[i] != '#') {
ans += s[i];
}
}
return ans;
}
};
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 # 返回臂长,此臂长不包括回文串中心点
def longestPalindrome(self, s: str) -> str:
end, start = -1, 0 # 最长回文子串起始位置
s = '#' + '#'.join(list(s)) + '#' # 插入特殊字符
arm_len = [] # 记录每点的最大臂长
right = -1 # 右边界
j = -1 # 右边界的中心结点
for i in range(len(s)):
if right >= i: # 若 i 在当前最大回文子串的臂长的右边界内
i_sym = 2 * j - i # 求出 i 关于 j 的对称点
min_arm_len = min(arm_len[i_sym], right - i) # i 的最小臂长为 min(arm_len[i_sym], right - i)
cur_arm_len = self.expand(s, i - min_arm_len, i + min_arm_len) # 在最小臂长的基础上暴力扩展
else: # 若 i 不在当前最大回文子串的臂长的右边界内
cur_arm_len = self.expand(s, i, i) # 直接以 i 为中心进行暴力扩展
arm_len.append(cur_arm_len) # 将求出的 i 的最大臂长放入数组 arm_len 中
if i + cur_arm_len > right: # 若 i 的右边界超过了原来的右边界,则更新中心点为 i ,更新右边界为 i 的右边界
j = i
right = i + cur_arm_len
if 2 * cur_arm_len + 1 > end - start: # 若以 i 为中心的最大回文串长度已经超过了原来最长的,更新起始位置为 i 的
start = i - cur_arm_len
end = i + cur_arm_len
return s[start+1:end+1:2] # 返回最大回文串
class Solution {
public String longestPalindrome(String s) {
int start = 0, end = -1;
StringBuffer t = new StringBuffer("#");
for (int i = 0; i < s.length(); ++i) {
t.append(s.charAt(i));
t.append('#');
}
t.append('#');
s = t.toString();
List arm_len = new ArrayList();
int right = -1, j = -1;
for (int i = 0; i < s.length(); ++i) {
int cur_arm_len;
if (right >= i) {
int i_sym = j * 2 - i;
int min_arm_len = Math.min(arm_len.get(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.add(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;
}
}
StringBuffer ans = new StringBuffer();
for (int i = start; i <= end; ++i) {
if (s.charAt(i) != '#') {
ans.append(s.charAt(i));
}
}
return ans.toString();
}
public int expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return (right - left - 2) / 2;
}
}
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
}