问题描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
中心点扩展算法
思想
- 选择一个中心点,向两边扩展,直至不符合回文串要求;
- 时间复杂度:O(n2);空间复杂度:O(1);
源码
//中心点扩展
//时间复杂度:O(n)
//空间复杂度:O(1)
string longestPalindrome(const string& str) {
if (str.length() < 2) return str;
int start = 0, len = 0;
int slen = 2 * str.length() - 1;
for (int i = 0; i < slen; ++i) {
int left = i / 2;
int right = (i + 1) / 2;
//向外扩展,直至不符合回文串要求
while (left >= 0 && right < str.length() && str[left] == str[right]) {
--left;
++right;
}
++left;
if (len < (right - left)) {
len = right - left;
start = left;
}
}
return str.substr(start, len);
}
Manacher算法
思想
- Manacher算法是在O(n)时间内解决寻找源字符串的最长回文子串S的问题的算法。
- Manacher将偶回文串和奇回文串统一转换为奇回文串的计算;并利用对称的思想,当已知字符串前i个字符中的中心回文串的最大右边界时,可以先对当前中心点i的回文串的半径进行初步更新,以减少扩展次数;
if (i < c + dp[c]) dp[i] = min(dp[2 * c - i], dp[c] + c - i);
其中,c为已知的最大右边界对应的回文串的中心点;
实现步骤
- 首先,为方便计算偶回文串和奇回文串的情况,将字符串转换为以‘#’间隔的字符串,并在首尾分别插入’$'以及‘^’以自动结束扩展,而不必判断终止条件;
例如:字符串“abcd”转换为“$#a#b#c#d#^”;即原字符串中第i个位置处于新字符串的第2*i+2个位置;
我们也可以知道在新字符串中,回文串必然是以#开始,以#结束的,且#必然在奇数位;如:[#a…a#],设left=res-dp[rc]+1和right=rc+dp[rc]-1分别为#开始和结束的坐标;那么转换为原数据str上的索引为start=left/2,end=right/2; - 创建dp数组,dp[i]表示为i为中心的最大回文串的半径;
- 记录当前最大右边界对应回文串的中心点c;
- 算法核心:如果当前中心点处于已知回文串最大右边界内,则直接采用以下公式更新dp[i]:
dp[i] = min(dp[2 * c - i], dp[c] + c - i); - 此时,dp[i]表示当前以i为中心点的回文串的半径;并在此基础上进一步向外扩展;
- 更新当前最大右边界对应回文串的中心点c以及最大回文子串的中心点;
参考:
https://segmentfault.com/a/1190000008484167?utm_source=tag-newest
https://blog.csdn.net/mlm5678/article/details/89820921
源码
//Manacher
//时间复杂度:O(n)
//空间复杂度:O(n)
string longestPalindrome(const string& str) {
if (str.length() < 2) return str;
//构建Manacher数据
string s(2 * str.length() + 3, '#');
s[0] = '$'; s.back() = '^'; //方便自动退出
for (int i = 0; i < str.length(); ++i)
s[2 * i + 2] = str[i];
vector<int>dp(s.length(), 1);
dp[0] = -1;
//当前最大右边界对应回文串的中心点
int c = 0;
//最大回文子串的中心点
int rc = 0;
for (int i = 1; i < s.length(); ++i) {
//利用已知,Manacher核心
if (i < c + dp[c])
dp[i] = min(dp[2 * c - i], dp[c] + c - i);
int left = i - dp[i];
int right = i + dp[i];
//向外扩展
while (s[left] == s[right]) {
--left;
++right;
++dp[i];
}
if (dp[c] + c < i + dp[i])
c = i;
if (dp[rc] < dp[i])
rc = i;
}
//最大回文子串必然以#开头,以#结束,且#在奇数位置
//如:[#a...a#],设left=res-dp[rc]+1和right=rc+dp[rc]-1分别为#开始和结束的坐标
//那么转换为原数据str上的索引为start=left/2,end=right/2
int start = (rc - dp[rc] + 1) / 2;
int end = (rc + dp[rc] - 1) / 2;
return str.substr(start, end - start);
}