Manacher算法
2019-06-12 本文已影响0人
topshi
前言
Manacher算法用于求解字符串中最长的回文子串,它和KMP算法的改进方式很类似,都是基于暴力求解的方法,通过利用历史信息进行优化。所谓回文子串是指以子串中间字符为中心,左右两端是对称的。例如,cbcacb
的最长回文子串为bcacb
,其长度为5。
相关题目:5. 最长回文子串
最长回文子串的暴力求解
-
遍历字符串中的每个字符,比较其左右关于该字符为中心对称的位置的字符是否相同,如果相同比较该字符的左边的左边和右边的右边字符是否相同,以这种向外“扩”的方式计算以每个字符为中心的回文子串长度。
暴力求解的时间复杂度是O(n * n)
Manacher算法
Manacher算法基于暴力求解做出优化,它定义了如下几个概念:
- 回文半径:以某个字符为中心向外扩的字符个数称为字符的回文直径,那么回文半径不言而喻。例如
cbcacb
,以字符a
为中心向外扩,可以扩c
,再扩b
,再加上字符本身,字符a
的回文半径是3。 - 回文半径数组
pArr
:记录以每个字符为中心的回文半径。例如上图的回文半径数组为[1,2,1,3,1,1]
. - 最右回文右边界
R
:向外扩的过程中,扩到的最右字符的下标。例如字符串cbcacb
,遍历到第一个c
,其只能扩到自己,则R = 0
;遍历到b
时,可以扩一个c
,则R = 2
.(一旦能扩到更右的字符,必须更新R
) - 最右回文右边界对应的回文中心
C
:C
和R
是对应的,R
正是中心C
所能扩到的最右字符下标,因此R
和C
是同时更新的。
我们的“扩”是基于一个中心向外扩的,扩出来的回文子串长度必是奇数。为了处理回文子串为偶数的情况,我们在字符间插入一个字符#
以保证回文子串长度都是奇数。例如,字符串cbcacb
- > #c#b#c#a#c#b#
。
Manacher算法如何利用pArr
、C
和R
对查找进行加速
我们以指针cur
去遍历字符串,
- 情况1:
当cur
位于R
的右边时,这种情况无法使用回文半径数组pArr
进行加速,只能暴力向外扩。 - 情况2:
当cur
位于R
的左边时,此时可以利用回文半径数组pArr
来进行加速处理。
首先,画出R
、cur
关于中心C
的对称点,- 如果以
cur'
为中心的回文子串的左边界没有超出L
(也没有压线),如下图
那么,pArr[cur] = pArr[cur']
。
证明:首先,L -> C
和C -> R
这两块是对称的,身为子块的x' -> y'
的对称块为y -> x
,所以pArr[cur] = pArr[cur']
。
疑问:pArr[cur]
有没有可能比pArr[cur']
大?不可能,假设y前面的一个字符 == x后面的一个字符
,那当时扩cur'
时,为什么不扩,它们是对称的。 - 如果以
cur'
为中心的回文子串的左边界超出了L
,如下图
那么,pArr[cur] = R - cur + 1
。
证明:假设R
的右边一个字符为x
,它的对称字符为x'
,根据pArr[C]
,x' != x
;又x' == y'
,y' == y
,推出x != y
。因此,以cur
为中心的回文半径为R - cur + 1
。 - 如果以
cur'
为中心的回文子串的左边界刚好压中L
,如下图
这种情况下,y的前一个字符和x的后一个字符的关系无法通过历史信息推算出来,只知道以cur
为中心的回文子串半径至少为R - cur + 1
,因此需要继续向外扩以确定pArr[cur]
。
- 如果以
综上所述,分四种情况:
-
cur
在R
右边,暴力扩 -
cur
在R
左边-
cur'
回文左边界没超过L
,pArr[cur] = pArr[cur']
-
cur'
回文左边界超出L
,pArr[cur] = R - cur + 1
-
cur'
回文左边界压中L
,pArr[cur] = R - cur + 1 + 继续向外扩
-
时间复杂度
- 遍历了一遍字符串,O(n)
相关题目
class Solution {
public String longestPalindrome(String s) {
if(s.length() < 2) return s;
char[] m = manacherString(s);
int[] pArr = new int[m.length];
int c = -1;
int r = -1;
int maxLen = -1;
StringBuilder maxStr = new StringBuilder();
for(int i = 0;i != m.length;i++){
//i在r右边,直接1;否则就是pArr[cur']或R - cur + 1
pArr[i] = i < r ? Math.min(pArr[2 * c - i], r - i + 1) : 1;
//看能不能扩(针对压中L和暴力扩的情况)
while(i - pArr[i] >= 0 && i + pArr[i] < m.length
&& m[i - pArr[i]] == m[i + pArr[i]]){
pArr[i]++;
}
if(i + pArr[i]- 1 > r){
c = i;
r = i + pArr[i] - 1;
}
if(maxLen <= pArr[i]){
maxLen = pArr[i];
maxStr.delete(0,maxStr.length());
for(int j = i - pArr[i]+1;j < r;j++){
if(m[j] != '#'){
maxStr.append(m[j]);
}
}
}
}
return maxStr.toString();
}
private char[] manacherString(String s){
char[] m = new char[2 * s.length() + 1];
for(int i = 0;i < m.length;i++){
m[i] = i % 2 == 0? '#' : s.charAt(i / 2);
}
return m;
}
}