首先,我们需要明确回文子串的定义。回文指的是一个字符串正读和反读都相同,而子串则是指字符串中的连续字符序列。以字符串"babad"为例,其最长回文子串是"bab"。
求解最长回文子串最直观的方法是暴力枚举所有子串并判断是否为回文,这种方法的时间复杂度为O(n^2)。虽然对于最大数据量1e3的情况可以通过,但并非最优解。本文将重点讨论Manacher算法,该算法利用回文的对称性进行记忆和动态扩展,能够在O(n)时间复杂度内解决问题。
Manacher算法的具体实现步骤如下:
-
预处理:在源字符串的每个字符间插入无关字符(如#),并在首尾分别插入两个不同的字符(如^和$)。这样处理后,无论原字符串长度为奇数还是偶数,都能统一处理为奇数长度。
-
利用对称性:由于回文具有对称性,算法维护一个最右边界变量和中心位置。通过已知的回文信息,可以借助新位置的对称位置在O(1)时间内判断新位置的回文性。
-
中心扩展:如果无法通过对称性获取新位置的回文信息,则通过比较当前位置的左右字符来判断是否满足回文条件,并动态扩展回文中心。
完成Manacher算法后,我们得到扩展字符串 t 的最长回文子串的中心位置下标max_i,以及以max_i为中心的最长回文半径。接下来需要将 t 的信息映射回原字符串,下面讨论二者的映射关系。
举例说明:原字符串 s = "babad",预处理后 t = "^#b#a#b#a#d#$"。在 s 中第一个 a 的下标为1,在 t 中为4。由于 t 是通过在 s 的每两个字符间插入#形成的,因此二者下标呈线性关系:
t [i] = k * s[i] + b。显然 k=2,b=2,得到t[i] = 2 * s[i] + 2,s[i] = (t[i] - 2) / 2。注意到,这里b即为最长回文中心的halfLen。
最长回文子串的中心下标为 max_i = 4 或 max_i = 6,其半径 h1 = 4。t 中最长回文的起始位置为 left = max_i - h1。根据 t 与 s 的下标映射关系,s 中对应的最长回文起始位置为 (left - 2)/2 = (max_i - h1 - 2)/2。由于 t 的首字符 "^" 为无效位置,即 t[2] 对应 s[0],实际映射关系应为 (max_i - h1)/2。t 中最长回文的半径为 h1,其长度为 2*h1 + 1(包含回文中心)。相应地,s 中最长回文的长度为 h1 - 1。例如,在示例中,s 的最长回文 "bab" 或 "aba" 的长度为 3,即 h1 - 1。
代码:
class Solution {
public:
string longestPalindrome(string s) {
//预处理
string t = "^";
for (char c:s) {
t += "#";
t += c;
}
t += "#$";
//halfLen[i] 第 i 个位置的最长回文半径
vector<int> halfLen(t.size() - 2);
halfLen[1] = 1;
//box_r 最长回文的最右端的下一个,box_m 当前最长回文的中心,max_i 全体最长回文中心
int box_r = 0, box_m = 0, max_i = 0;
for (int i = 2;i < halfLen.size();i++) {
int h1 = 1;//当前位置的初始回文半径
//对称性获得半径
if (i < box_r) {
//取最小,保证半径有效
h1 = min(halfLen[2 * box_m - i],box_r - i);
}
//中心扩展
while (t[i - h1] == t[i + h1]) {
h1 += 1;
box_m = i,box_r = i + h1;
}
halfLen[i] = h1;
if (h1 > halfLen[max_i]) {
max_i = i;
}
}
int h1 = halfLen[max_i];
return s.substr((max_i - h1) / 2,h1 - 1);
}
};
时间复杂度:O(n),n为 s 长度
空间复杂度:O(n),存储半径