什么是manacher算法
用于快速计算一个字符串的最长回文子串
什么是最长回文子串?
例如:abc12321中,最长回文子串为12321,即子字符串中最长,且是回文的那个
怎么用暴力做法找出最长回文子串呢?
- 长度为奇数的字符串:枚举每个位置的字符,往两边扩,一直扩到不是回文为止,记录扩的最长的那个子串
- 长度为偶数的字符串:除了枚举每个位置的字符外,还要枚举每两个字符中间的位置,例如"1221",如果只枚举每个字符,最长的长度为1,但从两个"2"的中间位置开始扩,能计算出正确答案4
这种做法时间复杂度为O(n^2)
,而manacher算法能做到时间复杂度为O(n)
为了方便计算长度为偶数的字符串的最长回文子串,将原始字符串处理成左右两边加上#
,且每两个字符中间也加上#
这样不管原始串长度为奇数还是偶数,都可以用枚举每个字符往外扩的方式计算最长回文子串
几个概念
-
回文半径数组pArr
:以每个字符为中心,能扩出来的的最大回文半径 -
最右回文边界R
:每个字符往左右扩时,扩的最右的位置- 不管是以哪个字符为中心扩的,只要比以前历次扩得更往右了,就增加最右回文边界R
-
取得最右回文边界时的中心位置C
- 随着R的更新而更新
流程
manacher算法的整体流程,和暴力做法很相似,都是遍历每个位置,以每个位置为中心往外扩
但是可以利用上文提到的pArr,R,C
进行加速
遍历到每一个字符时,有以下的可能性:
i位置比R位置大
这种情况无法利用以前的数据优化,暴力往外扩
i位置小于等于R
假设遍历到i时,发现i小于等于R,说明和R对应的C,一定在i左边,因为一定之前i左边的一个数作为C,的最右回文边界扩到了R位置
作出一个i
关于C的对称点 i*
,和R关于C的对称点L,如下图所示:
i的回文半径可以查表得出,因为在之前遍历到i时,一定计算过i*的回文半径
接下来根据i*的回文半径分情况讨论
情况一
i*
扩出来的区域,在(L,R)之间,即左边界大于L
假设i*
扩出来的区域为A,作出以i
位置为中心,和区域A等长度的区域B
由于A和B关于C对称,则A和B互为逆序
因为A为回文,根据回文的逆序也是回文这一特点,推出B也是回文
说明以i
为中心扩出来的区域,至少有和A一样的长度是回文
那有没有可能更长呢?
假设A区域左边的字符为x,右边的字符为y
B区域左边的字符为m,右边的字符为n
当初i*
没能再往两边扩,说明 x != y
根据回文的对称性,y == m,x == n
,因为x不等于y,说明m不等于n,则B区域无法再往外扩
因此以i为中心的最长回文半径,就是i*的最长回文半径:pArr[i] = pArr[i*]
情况二
i*
扩出来区域的左边界比L更小:
假设L关于i*
对称的点位L*,L左边为a,L*右边为b
a和b是什么关系?相等,因为以i*为中心往外扩出来的区域,远不止a和b,因此a和b是关于i*
对称的,相等
我们看右边,假设R关于i
的对称点为R*,假设从L到L的区域为A,从R到R的区域为B
因为A和B关于C对称,因此A和B互为逆序
且L到L关与i对称,因此A为回文串,根据回文的逆序也是回文的规则,B也为回文
因此以i为中心,至少有A区域长度的回文子串
有没有可能更长呢?
接下来思考x和y是否相等
a和b在i*的回文半径内,因此a == b
而b和x回文对称,因此b == x
,推出a == x
当初为什么以C为半径没能再往外扩了,就是因为a != y
,而a == x
, 推出x != y
说明以i
为中心的最大回文串最多R,无法再往外扩,i
为中心的最长回文半径,就是从i
到R
的距离
情况三
i*
扩出来区域的左边界和L重合:
根据上文分析的性质,关于i
至少有B区域为回文串
但是会不会更大呢?不确定,需要往外扩尝试!
为什么此时不确定呢?
假设L左边的字符为a,L*右边的字符为b
R*左边的字符为x,R右边的字符为y
因为之前i*
没能在往外扩,因此a != b,而b等于x,则a != x
因为之前C没能在往外扩,因此a != y
目前能得到的结论是:a != x,a != y
,那x和y是否相等呢?无法确定!
因此到底以i为中心的最长回文子串有多长,需要通过往外扩才知道
总结
根据上面的分析,manacher算法的流程可以总结为:
-
i位置比R位置大:暴力扩
-
i位置小于等于R
- i扩出来的区域,在(L,R)之间:i的回文半径就是i的回文半径
- i*扩出来的区域的左边界比L更小:i的回文半径就是从i到R的距离
- i*扩出来区域的左边界和L重合:i的回文半径至少为从i到R的距离,至于有没有可能更长需要往外扩尝试
时间复杂度分析
对于遍历到的每个位置,一定会走上面4个分支中的一个
分支2.a和2.b直接查表pArr得到答案,耗时O(1)
分支1和2.c要么扩失败,要么会不断推高R,而R是有极限的,最多被推高O(N)次
同时扩失败的总次数也是有限的,最多为O(N)次
因此整体时间复杂度为O(N)
代码
public static int manacher(String s) {
char[] str = preHandle(s);
int[] pArr = new int[str.length];
int C = -1;
int R = -1;
for (int i = 0;i<str.length;i++) {
// i比R大
if (i > R) {
int l = i;
int r = i;
// 不断往外扩
while (l >= 0 && r < str.length && str[l] == str[r]) {
R = r;
C = i;
pArr[i] = R - C;
r++;
l--;
}
continue;
}
// i小于等于R
int _i = 2 * C - i;
int L = 2 * C - R;
// _i扩的左边界比L大
if (L < _i - pArr[_i]) {
pArr[i] = pArr[_i];
// _i扩的左边界比L大小
} else if (L > _i - pArr[_i]) {
pArr[i] = R - i;
// _i扩的左边界和L相等
} else {
pArr[i] = R - i;
int l = i - pArr[i] - 1;
int r = R + 1;
// 不断往外扩
while (l >= 0 && r < str.length && str[l] == str[r]) {
R = r;
C = i;
pArr[i] = R - C;
r++;
l--;
}
}
}
int max = Integer.MIN_VALUE;
for (int i = 0;i<pArr.length;i++) {
max = Math.max(max, pArr[i] * 2 + 1);
}
return max / 2;
}
/**
*abcba => #a#b#c#b#a#
*/
private static char[] preHandle(String s) {
char[] res = new char[2 * s.length() + 1];
res[0] = '#' ;
int resi = 1;
for (int i = 0;i < s.length();i++) {
res[resi] = s.charAt(i);
resi++;
res[resi] = '#' ;
resi++;
}
return res;
}