# 经典算法问题：最长回文子串之 Manacher 算法

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

LeetCode 第 5 题就是“最长回文子串”的模板题。

## LeetCode 第 5 题：最长回文子串

``````输入: "babad"

``````

``````输入: "cbbd"

``````

Python 代码：

``````class Solution:
def longestPalindrome(self, s):
"""
最长回文子串，比较容易想到的就是中心扩散法
:type s: str
:rtype: str
"""
size = len(s)
if size == 0:
return ''

# 至少就是 1
longest_palindrome = 1

longest_palindrome_str = s[0]

for i in range(size):
palindrome_odd, odd_len = self.__center_spread(s, size, i, i)
palindrome_even, even_len = self.__center_spread(s, size, i, i + 1)

# 当前找到的最长回文子串
cur_max_sub = palindrome_odd if odd_len >= even_len else palindrome_even
if len(cur_max_sub) > longest_palindrome:
longest_palindrome = len(cur_max_sub)
longest_palindrome_str = cur_max_sub

return longest_palindrome_str

def __center_spread(self, s, size, left, right):
"""
left = right 的时候，表示回文中心是一条线，回文串的长度是奇数
right = left + 1 的时候，表示回文中心是任意一个字符，回文串的长度是偶数
:param s:
:param size:
:param left:
:param right:
:return:
"""
l = left
r = right

while l >= 0 and r < size and s[l] == s[r]:
l -= 1
r += 1
return s[l + 1:r], r - l - 1
``````

Java 代码：

``````public class Solution {

public String longestPalindrome(String s) {
int len = s.length();
if (len == 0) {
return "";
}
int longestPalindrome = 1;
String longestPalindromeStr = s.substring(0, 1);
for (int i = 0; i < len; i++) {
String palindromeOdd = centerSpread(s, len, i, i);
String palindromeEven = centerSpread(s, len, i, i + 1);
String maxLen = palindromeOdd.length() > palindromeEven.length() ? palindromeOdd : palindromeEven;
if (maxLen.length() > longestPalindrome) {
longestPalindrome = maxLen.length();
longestPalindromeStr = maxLen;
}
}
return longestPalindromeStr;
}

private String centerSpread(String s, int len, int left, int right) {
int l = left;
int r = right;
while (l >= 0 && r < len && s.charAt(l) == s.charAt(r)) {
l--;
r++;
}
// 这里要特别小心，跳出 while 循环的时候，是第 1 个满足 s.charAt(l) != s.charAt(r) 的时候
// 所以，不能取 l，不能取 r
return s.substring(l + 1, r);
}
}
``````

`dp[i, j]`：如果子串 `s[i,...,j]` 是回文串，那么 `dp[i, j] = true`。即二维 dp：`dp[i, j]` 表示子串 `s[i, j]`（包括区间左右端点）是否构成回文串，是一个二维布尔型数组

`dp[i, j] = dp[i+1, j-1]`，当然，此时我们要保证 `[i+1, j-1]` 能够形成区间，因此有

`i+1<=j-1`，整理得 `i-j <= -2`，或者 `j-i >=2`

Python 代码：

``````class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
size = len(s)
if size <= 1:
return s
# 二维 dp 问题
# 状态：dp[i,j]: s[i:j] 包括 i，j ，表示的字符串是不是回文串
dp = [[False for _ in range(size)] for _ in range(size)]

longest_l = 1
res = s[0]

for i in range(size):
for j in range(i):
# 状态转移方程：如果头尾字符相等并且中间也是回文
# 或者中间的长度小于等于 1
if s[j] == s[i] and (j >= i - 2 or dp[j + 1][i - 1]):
dp[j][i] = True
if i - j + 1 > longest_l:
longest_l = i - j + 1
res = s[j:i + 1]
return res
``````

Java 代码：

``````public class Solution2 {

public String longestPalindrome(String s) {
int len = s.length();
if (len == 0) {
return "";
}
int longestPalindrome = 1;
String longestPalindromeStr = s.substring(0, 1);
boolean[][] dp = new boolean[len][len];
// abcdedcba
//   j   i
// 如果 dp[j,i] = true 那么 dp[j+1,i-1] 也一定为 true
// [j+1,i-1] 一定要构成至少两个元素额区间（ 1 个元素的区间，s.charAt(i)==s.charAt(j) 已经判断过了）
// 即 j+1 < i-1，即 i > j + 2 (不能取等号，取到等号，就退化成 1 个元素的情况了)
// 应该反过来写
for (int i = 0; i < len; i++) {
for (int j = 0; j <= i; j++) {
// 区间应该慢慢放大
if (s.charAt(i) == s.charAt(j) && (i <= j + 2 || dp[j + 1][i - 1])) {
// 写成 dp[j][i] 就大错特错了，不要顺手写习惯了
dp[j][i] = true;
if (i - j + 1 > longestPalindrome) {
longestPalindrome = i - j + 1;
longestPalindromeStr = s.substring(j, i + 1);
}
}
}
}
return longestPalindromeStr;
}
}
``````

Manacher 算法就是专门解决“最长回文子串”的一个算法，它的时间复杂度可以达到 ，虽然是特性的算法，并不具有普遍性，但它的代码量小，处理技巧优雅，是值得我们学习的。

## Manacher 算法

[Manacher(1975)] 发现了一种线性时间算法，可以在列出给定字符串中从字符串头部开始的所有回文。并且，Apostolico, Breslauer & Galil (1995) 发现，同样的算法也可以在任意位置查找全部最大回文子串，并且时间复杂度是线性的。因此，他们提供了一种时间复杂度为线性的最长回文子串解法。替代性的线性时间解决 Jeuring (1994), Gusfield (1997)提供的，基于后缀树(suffix trees)。也存在已知的高效并行算法。

Manacher 算法本质上还是中心扩散法，只不过它使用了类似 KMP 算法的技巧，充分挖掘了已经进行回文判定的子串的特点，使得算法高效。

### 第 1 步：预处理，添加分隔符

1、分隔符是字符串中没有出现过的字符，这个分隔符的种类只有一个，即你不能同时添加 `"#"``"?"` 作为分隔符；

2、在字符串的首位置、尾位置和每个字符的“中间”都添加 个这个分隔符，可以很容易知道，如果这个字符串的长度是 `len`，那么添加的分隔符的个数就是 `len + 1`，得到的新的字符串的长度就是 `2len + 1`，显然它一定是奇数。

1、首先是正确性：添加了分隔符以后的字符串的回文性质与原始字符串是一样的。

2、其实是避免奇偶数讨论，对于使用“中心扩散法”判定回文串的时候，长度为奇数和偶数的判定是不同的，添加分隔符可以避免对奇偶性的讨论。

### 第 2 步：得到 p 数组

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1 2 5
p-1

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1
p-1

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2
p-1

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1
p-1

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1 2
p-1

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1 2 5
p-1

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1 2 5 2 1 6 1 2 3 2 1
p-1

p-1 数组很简单了，把 p 数组的数 -1 就行了。实际上直接把能走的步数记录下来就好了。不过就是为了给“回文半径”一个定义而已。

char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1 2 5 2 1 6 1 2 3 2 1
p-1 0 1 0 1 4 1 0 5 0 1 2 1 0

### 如何编写程序得到 p 数组？

`id` ：从开始到现在使用中心扩散法得到的最长回文子串的中心的位置；

`mx`：从开始到现在使用中心扩散法得到的最长回文子串能延伸到的最右端的位置。

`p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;`

image-20190217104141751

1、首先当 `i` 位于 `id``mx` 之间时，此时 `id` 之前的 `p` 值都已经计算出来了，我们利用已经计算出来的 `p` 值来计算当前考虑的位置的 `p` 值。

`id < i < mx` 的时候：

``````if i < mx:
p[i] = min(p[2 * id - i], mx - i);
``````

`j` 的范围很小的时候，取 `p[2 * id - i]` ，此时 `p[i] = p[j]`

2、当 `mx - i > p[j]` 的时候，以 `s[j]` 为中心的回文子串包含在以 `s[id]` 为中心的回文子串中，由于 `i``j` 对称，以 `s[i]` 为中心的回文子串必然包含在以 `s[id]` 为中心的回文子串中，所以必有 `p[i] = p[j]`，见下图。

image-20190217110809070
image-20190217104121589

3、当 `p[j] >= mx - i` 的时候，以 `s[j]` 为中心的回文子串不一定完全包含于以 `s[id]` 为中心的回文子串中，但是基于对称性可知，下图中两个绿框所包围的部分是相同的，也就是说以 `s[i]` 为中心的回文子串，其向右至少会扩张到 `mx` 的位置，也就是说 `p[i] >= mx - i`。至于 `mx` 之后的部分是否对称，就只能老老实实去匹配了。

image-20190217110822710

4、对于 `mx <= i` 的情况，无法对 `p[i]` 做更多的假设，只能从 `p[i] = 1` 开始，然后再去匹配了。

Java 代码：

``````/**
* 使用 Manacher 算法
*/
public class Solution3 {

/**
* 创建分隔符分割的字符串
*
* @param s      原始字符串
* @param divide 分隔字符
* @return 使用分隔字符处理以后得到的字符串
*/
private String generateSDivided(String s, char divide) {
int len = s.length();
if (len == 0) {
return "";
}
if (s.indexOf(divide) != -1) {
throw new IllegalArgumentException("参数错误，您传递的分割字符，在输入字符串中存在！");
}
StringBuilder sBuilder = new StringBuilder();
sBuilder.append(divide);
for (int i = 0; i < len; i++) {
sBuilder.append(s.charAt(i));
sBuilder.append(divide);
}
return sBuilder.toString();
}

public String longestPalindrome(String s) {
int len = s.length();
if (len == 0) {
return "";
}
String sDivided = generateSDivided(s, '#');
int slen = sDivided.length();
int[] p = new int[slen];
int mx = 0;
// id 是由 mx 决定的，所以不用初始化，只要声明就可以了
int id = 0;
int longestPalindrome = 1;
String longestPalindromeStr = s.substring(0, 1);
for (int i = 0; i < slen; i++) {
if (i < mx) {
// 这一步是 Manacher 算法的关键所在，一定要结合图形来理解
// 这一行代码是关键，可以把两种分类讨论的情况合并
p[i] = Integer.min(p[2 * id - i], mx - i);
} else {
// 走到这里，只可能是因为 i = mx
if (i > mx) {
throw new IllegalArgumentException("程序出错！");
}
p[i] = 1;
}
// 老老实实去匹配，看新的字符
while (i - p[i] >= 0 && i + p[i] < slen && sDivided.charAt(i - p[i]) == sDivided.charAt(i + p[i])) {
p[i]++;
}
// 我们想象 mx 的定义，它是遍历过的 i 的 i + p[i] 的最大者
// 写到这里，我们发现，如果 mx 的值越大，
// 进入上面 i < mx 的判断的可能性就越大，这样就可以重复利用之前判断过的回文信息了
if (i + p[i] > mx) {
mx = i + p[i];
id = i;
}

if (p[i] - 1 > longestPalindrome) {
longestPalindrome = p[i] - 1;
longestPalindromeStr = sDivided.substring(i - p[i] + 1, i + p[i]).replace("#", "");
}
}
return longestPalindromeStr;
}
}
``````

## 参考资料

Manacher's Algorithm 马拉车算法 - Grandyang - 博客园

https://www.cnblogs.com/grandyang/p/4475985.html

【看这篇文章就可以看懂】

Manacher算法总结 - CSDN博客 https://blog.csdn.net/dyx404514/article/details/42061017

Manacher算法及其Java实现 - CSDN博客 https://blog.csdn.net/simaxiaochen/article/details/62043408

1、https://subetter.com/articles/2018/03/manacher-algorithm.html（这篇文章的图最好了，把关键的部分和 代码都给出来了）

5、动态规划的做法：

https://www.geeksforgeeks.org/longest-palindrome-substring-set-1/

6、也有图和 Python 代码

https://segmentfault.com/a/1190000003914228

https://segmentfault.com/a/1190000003914228