前言
给定一个字符串,求出其最长回文子串。例如:
s="abcd",最长回文长度为 1;
s="ababa",最长回文长度为 5;
s="abccb",最长回文长度为 4,即 bccb。
以上问题的传统思路大概是,遍历每一个字符,以该字符为中心向两边查找。其时间复杂度为 ,效率很差。
1975 年,一个叫 Manacher 的人发明了一个算法,Manacher 算法(中文名:马拉车算法),该算法可以把时间复杂度提升到 。下面来看看马拉车算法是如何工作的。
传统思路
i自增,从左到右,然后每个i为中心,取得最长的回文长度并记录起止节点。最多要进行两次n的遍历,时间复杂度为
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function(s) {
if (s == null || s.length < 1) { // 传入空直接返回''
return '';
}
let start = 0;
let end = 0;
for (let i = 0; i < s.length; i++) {
let len1 = expandAroundCenter(s, i, i); // 奇回文
let len2 = expandAroundCenter(s, i, i + 1); // 偶回文
// 分别以i为中心点取最长的奇偶回文长度
let len = Math.max(len1, len2);
if (len > (end - start)) { // 若比上一段取得回文长
start = i - Math.floor((len - 1) / 2);
end = i + Math.floor(len / 2);
}
}
// 根据取得的最长回文左右两个节点取字符串
return s.substring(start, end + 1);
};
// 已中心扩展判断最长回文长度
function expandAroundCenter(s, left, right) {
var L = left,
R = right;
while (L >= 0 && R < s.length && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
Manacher的思路
首先因为处理奇回文和偶回文会比较繁琐,需要分开处理。因此在首尾和各字符间插入#,将长度统一转换为奇数(算法里常用的解决奇偶繁琐问题的处理手段)
然后定义一个辅助数组p[],其中p[i]对应为以i为中心的最长回文长度。

p[i]-1就是原字符串中最长回文串的长度。
然后整个工作的重点就是求解p数组:

设置两个变量,mx 和 id 。mx 代表以 id 为中心的最长回文的右边界,也就是mx = id + p[id]。
假设我们现在求p[i],也就是以 i 为中心的最长回文半径,如果i < mx,如上图,那么:
if (i < mx) p[i] = min(p[2 * id - i], mx - i);
2 * id - i为 i 关于 id 的对称点,即上图的 j 点,而p[j]表示以 j 为中心的最长回文半径,因此我们可以利用p[j]来加快查找。
代码
shutup and show me the code
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
char s[1000];
char s_new[2000];
int p[2000];
int Init()
{
int len = strlen(s);
s_new[0] = '$';
s_new[1] = '#';
int j = 2;
for (int i = 0; i < len; i++) {
s_new[j++] = s[i];
s_new[j++] = '#';
}
s_new[j] = '\0'; // 别忘了哦
return j; // 返回 s_new 的长度
}
int Manacher()
{
int len = Init(); // 取得新字符串长度并完成向 s_new 的转换
int max_len = -1; // 最长回文长度
int id;
int mx = 0;
for (int i = 1; i < len; i++) {
if (i < mx) {
p[i] = min(p[2 * id - i], mx - i);
// 需搞清楚上面那张图含义, mx 和 2*id-i 的含义
}
else {
p[i] = 1;
}
while (s_new[i - p[i]] == s_new[i + p[i]]) {
// 不需边界判断,因为左有'$',右有'\0'
p[i]++;
}
// 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
// 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
if (mx < i + p[i]) {
id = i;
mx = i + p[i];
}
max_len = max(max_len, p[i] - 1);
}
return max_len;
}
int main() {
while (printf("请输入字符串:\n")) {
scanf("%s", s);
printf("最长回文长度为 %d\n\n", Manacher());
}
return 0;
}
直接贴代码,核心部分就是
if (i < mx) { p[i] = min(p[2 * id - i], mx - i); }
这句公式。根据自增的i算出每个p[i]取其中的max。
整个算法中,i是自增的,每次循环要么在扩展右边界(代码中的变量mx),要么直接得出结论,所以算法时间可以到O(n)。
P.S.分享一篇公众号漫画
这个漫画也是逐步解释了算法实现的一步步过程,比较有意思。
心得总结
首先求最长回文的方法先要掌握普通的中心扩展探索的方法,马拉车算法在这个前提条件下加上了右边界的判断,在没有覆盖到的时候通过中心扩展同时更新右边界,最后得出的p[i]数组里取出最大值即为要求的最长回文长度。