题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
解题思想
考虑三种解决方案:
1.动态规划
设状态dp[j][i]表示索引j到索引i的子串是否是回文串。则易得转移方程如下:
d
p
[
j
]
[
i
]
=
{
t
r
u
e
,
j = i
s
t
r
[
i
]
=
=
s
t
r
[
j
]
,
i - j = 1
s
t
r
[
i
]
=
=
s
t
r
[
j
]
&
&
d
p
[
j
+
1
]
[
i
−
1
]
,
i - j > 1
dp[j][i] = \begin{cases} true, & \text{j = i} \\ str[i] == str[j], & \text{i - j = 1} \\ str[i] == str[j] \&\& dp[j + 1][i - 1], & \text{i - j > 1} \\ \end{cases}
dp[j][i]=⎩⎪⎨⎪⎧true,str[i]==str[j],str[i]==str[j]&&dp[j+1][i−1],j = ii - j = 1i - j > 1
则dp[j][i]为true时表示索引j到索引i形成的子串为回文子串,且子串起点索引为j,长度为i - j + 1。
2.中心扩展
枚举中心位置,然后再在该位置上向左右扩展,找到最长的回文子串,注意区分回文子串长度为奇数和偶数的两种情况。
3.Manacher算法
Manacher算法首先通过在每个字符的两边都插入一个特殊的符号,将所有可能的奇数或偶数长度的回文子串都转换成了奇数长度。比如"abba"变成"#a#b#b#a#",“aba"变成”#a#b#a#"。
以字符串s = "babad"为例,插入#这个特殊字符后,变成 t = “#b#a#b#a#d#”,然后用一个数组p[i]来记录以t[i]为中心的最长回文子串向左或向右扩张的长度(包括t[i]),即p[i]为新字符串t在t[i]处的回文半径。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
t | # | b | # | a | # | b | # | a | # | d | # |
p | 1 | 2 | 1 | 4 | 1 | 4 | 1 | 2 | 1 | 2 | 1 |
可以看到,p[i] - 1就是以t[i]为中心的回文子串在原字符串s中的长度。可以如下证明之:
首先在转换得到的字符串t中,所有的回文子串的长度都为奇数,那么对于以t[i]为中心的回文字串,其长度就为2*p[i] - 1,又可以观察到,t中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有p[i]个分隔符,剩下p[i] - 1个字符来自原字符串,所以该回文串在原字符串中的长度就为p[i]-1。
所以原问题就转化为求p[i] - 1的最大值。
Manacher算法增加两个辅助变量id和mx,其中id表示能延伸到最右端位置的那个回文子串的中心点位置,mx则为id + p[id],也就是回文子串能延伸到的最右端位置(即将到达但还没有到达)。得到一个很重要的结论:
如果mx > i,那么p[i] >= min(p[2 * id - i], mx - i)
下面来证明上述结论,令j = 2 * id - i,也就是说j是i关于id的对称点。
-
当mx - i > p[j] 的时候,以t[j]为中心的回文子串包含在以t[id]为中心的回文子串中,由于i和j对称,以t[i]为中心的回文子串必然包含在以t[id]为中心的回文子串中,所以必有p[i] = p[j];
-
当p[j] >= mx - i 的时候,以t[j]为中心的回文子串不一定完全包含于以t[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以t[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说p[i] >= mx - i。至于mx之后的部分是否对称,再具体匹配。
-
对于mx <= i 的情况,因为无法对p[i]做更多的假设,只能让p[i] = 1,然后再去匹配。
解题代码
class Solution {
public:
//DP
string longestPalindrome(string s) {
if(s.empty())
return "";
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int start = 0;
int maxLen = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j <= i ; j++){
if(i - j < 2)
dp[j][i] = (s[i] == s[j]);
else
dp[j][i] = (s[i] == s[j]) && dp[j + 1][i -1];
if(dp[j][i] && i - j + 1 > maxLen){
start = j;
maxLen = i - j + 1;
}
}
return s.substr(start, maxLen);
}
//中心扩展
string longestPalindrome2(string s) {
if(s.empty())
return "";
int n = s.size();
int start = 0;
int maxLen = 0;
for(int i = 0; i < n; i++){
//奇数扩展
for(int j = 0; i - j >= 0 && i + j < n; j++){
if(s[i - j] != s[i + j])
break;
if(maxLen < 2 * j + 1){
start = i - j;
maxLen = 2 * j + 1;
}
}
//偶数扩展
for(int j = 0; i - j >= 0 && i + j + 1 < n; j++){
if(s[i - j] != s[i + j + 1])
break;
if(maxLen < 2 * j + 2){
start = i - j;
maxLen = 2 * j + 2;
}
}
}
return s.substr(start, maxLen);
}
//马拉车算法
string longestPalindrome3(string s) {
if(s.empty())
return "";
//预处理
string t = "#";
for(auto c : s){
t += c;
t += "#";
}
int n = t.size();
int p[n];
int start = 0;
int maxLen = 0;
int id = 0; //能延伸到最右端位置的那个回文子串的中心点位置
int mx = 0; //回文子串能延伸到的最右端位置
for(int i = 0; i < n; i++){
// mx - i > p[2 * id - i]时,p[i] = p[2 * id - i]
// mx - i <= p[2 * id - i]时, p[i] >= mx - i
// 即mx > i 时,p[i] >= min(p[2 * id - i, mx - i])
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
while(i - p[i] >= 0 && i + p[i] < n && t[i - p[i]] == t[i + p[i]])
p[i]++;
if(mx < i + p[i]){
mx = i + p[i];
id = i;
}
if(maxLen < p[i] - 1){
maxLen = p[i] - 1;
start = (i - p[i] + 1) / 2;
}
}
return s.substr(start, maxLen);
}
};