5. 最长回文子串
题目描述
给你一个字符串 s
,找到 s
中最长的回文子串。
示例1
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例2
输入:s = "cbbd"
输出:"bb"
示例3
输入:s = "a"
输出:"a"
示例4
输入:s = "ac"
输出:"a"
提示:
提示:
- 1 ≤ s . l e n g t h ≤ 1000 1 \le s.length \le 1000 1≤s.length≤1000
s
仅由数字和英文字母(大写和/或小写)组成
题解:
Manacher算法,本质仍是暴力匹配。参考自: Manacher算法详解
应对偶数回文串方法:预处理,将原字符串的首部和尾部以及每两个字符之间插入一个特殊字符。
这一步预处理操作后的效果就是原字符串的长度从 n 改变成了 2*n+1,也就得到了我们需要的可以去做暴力扩展的字符串,并且从 预处理后的字符串得到的最长回文字符串的长度除以2 就是 原字符串的最长回文子串长度,也就是我们想要得到的结果。
概念:
回文半径和回文直径:因为处理后回文字符串的长度一定是奇数,所以回文半径是包括回文中心在内的回文子串的一半的长度,回文直径则是回文半径的2倍减1。比如对于字符串 “aba”,在字符 ‘b’ 处的回文半径就是2,回文直径就是3。
最右回文边界R:在遍历字符串时,每个字符遍历出的最长回文子串都会有个右边界,而R则是所有已知右边界中最靠右的位置,也就是说R的值是只增不减的。
回文中心C:取得当前R的第一次更新时的回文中心。由此可见R和C时伴生的。
半径数组:这个数组记录了原字符串中每一个字符对应的最长回文半径。
流程:
-
预处理原字符串
先对原字符串进行预处理,预处理后得到一个新的字符串,这里我们称为S,为了更直观明了的让大家理解Manacher的流程操作,我们在下文的S中不显示特殊字符(这样并不影响结果)。
-
R 和 C 的初始值为 -1 ,创建半径数组 pArr
这里有点与概念相差的小偏差,就是R实际是最右边界位置的右一位。
-
开始从下标 i = 0 去遍历字符串 S
- i > R,也就是 i 在 R 外,此时没有什么花里胡哨的方法,直接暴力匹配,记得看看 C 和 R 是否需要更新
-
i <= R,也就是 i 在 R 内,此时分三种情况,在讨论这三种情况前,我们先构建一个模型
L 是当前 R 关于 C 的对称点,i’ 是 i 关于 C 的对称点,可知 i’=2*C-i,并且我们发现,i’ 的回味区域是我们已经求过的,从这里我们就可以开始判断是不是可以进行加速处理了
2.1 i’ 的回文区域在 L-R 的内部,此时 i 的回文直径与 i’ 相同,我们可以直接得到 i 的回文半径,下面给出证明:
红线部分是 i’ 的回文区域,因为整个 L-R 就是一个回文串,回文中心是 C ,所以 i 形成的回文区域和 i’ 形成的回文区域是关于 C 对称的2.2 i’ 的回文区域左边界超过了 L ,此时 i 的回文半径则是 i 到 R ,下面给出证明:
首先我们设 L 点关于 i’ 的对称点为 L’,R点关于 i 点对称的点为 R’,L 的前一个字符为 x ,L’ 的后一个字符为 y ,k 和 z 同理。此时我们知道 L-L’ 是 i’ 回文区域内的一段回文串,故 R’-R 也是回文串,因为 L-R 是一个回文串,而它们都是对称关系。所以我们得到一系列关系:x=y,y=k,x!=z ,所以 k!=z 。这样可以验证 i 点的回文半径是 R-i
2.3 i’ 的回文区域左边界恰好和 L 重合,此时 i 的回文半径最少是 i 到 R,回文区域从 R 继续向外部匹配,下面给出证明:
因为 i’ 的回文左边界和 L 重合,所以已知的 i 的回文半径就和 i’ 的一样了,我们设 i 的回文区域右边界下一个字符是 y ,i 的回文区域左边界的上一个字符是 x ,现在我们只需要从 x 和 y 的位置开始暴力匹配,看是否能够把 i 的回文区域扩大即可。
Manacher算法代码模板:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500001;
char s1[N];
char s2[N << 1];
int p[N << 1];
int main(void) {
scanf("%s", s1);
int len = strlen( s1 ) * 2 + 1;
int k = 0;
for ( int i = 0; i < len; ++i ) {
s2[i] = ( i & 1 ) == 0 ? '#' : s1[k++];
}
int r = -1, c = -1;
int ret = 0;
for ( int i = 0; i < len; ++i ) {
p[i] = r > i ? min( r - i, p[2 * c - i]) : 1;
while ( i + p[i] < len && i - p[i] > -1 ) {
if ( s2[i + p[i]] == s2[i - p[i]] ) ++p[i];
else break;
}
if ( i + p[i] > r ) {
r = i + p[i];
c = i;
}
ret = max( ret, p[i] - 1 );
}
printf("%d\n", ret);
return 0;
}
回到这题上来,我们首先需要求出半径数组,找到最大的值 ret(减1表示实际的回文子串长度) 和下标 idx ,最长回文子串是:s[(idx - ret + 1) / 2, ret] 。
代码:
class Solution {
public:
void manacherString( vector<char>& t, string& s ) {
int n = s.length() * 2 + 1;
t.resize(n);
int k = 0;
for ( int i = 0; i < n; ++i )
t[i] = (i & 1) ? s[k++] : '#';
}
string manacher( const vector<char>& t, const string& s ) {
int r = -1, c = -1;
int ret = 0, len = t.size(), idx = -1;
vector<int> p( t.size() );
for ( int i = 0; i < t.size(); ++i ) {
p[i] = r > i ? min( r - i, p[2 * c - i] ) : 1;
while ( i + p[i] < len && i - p[i] >= 0 ) {
if ( t[i + p[i]] == t[i - p[i]] ) ++p[i];
else break;
}
if ( i + p[i] > r ) {
r = i + p[i];
c = i;
}
if ( p[i] > ret ) {
ret = p[i];
idx = i;
}
}
ret -= 1;
return s.substr( (idx - ret + 1) >> 1, ret );
}
string longestPalindrome(string s) {
if ( !s.size() ) return "";
vector<char> t;
manacherString( t, s );
return manacher( t, s );
}
};
/*
时间:16ms,击败:94.99%
内存:8.7MB,击败:73.13%
*/