这个算法是用来求一个字符串中的最长回文串的长度的,且时间复杂度是O(N)。
Manacher算法的预备概念
1. 最长回文半径长度数组
- 以字符串每个位置为中心向外扩散得到的最长的回文半径长度,例如有字符串abcbdadb,那么它的回文半径数组即为: [1, 1, 2, 1, 1, 3, 1, 1]
2. 最右回文右边界
- 直接看例子: 有字符串0123210,那么对于每个位置来说,最右回文右边界如下:
- 初始时最右回文右边界可置为-1,对于位置0,最右回文右边界变为1
- 位置1,最右回文右边界为2
- 位置2,最右回文右边界为3
- 位置3,最右回文右边界为7 (因为直到字符串结束都是以位置3为中心的回文)
- 位置4,最右回文右边界仍然为7
- 位置5,最右回文右边界为7
- 位置6,最右回文右边界为7
3. 最右回文右边界的最早到达点
- 与2是伴生的概念,对于2中例子来说,最右回文右边界为7的最早到达点就是位置3
Manacher算法
对于一个字符串,它的每一个位置i和最右回文右边界之间的关系只可能有两种情况:1. i不在最右回文右边界里; 2. i在最右回文右边界里
为了方便,将最右回文右边界即为R。
-
i不在最右回文右边界R里,此时没有什么加速的技巧,暴力扩张即可。
- 例子:有字符串 # 1 # 2 # 1 #,开始时,R为-1,而i=0,所以i不在R里。此时看看0位置能扩到哪,但它也只能扩到自己了,因为左面没有了嘛。所以0位置结束后,R由-1变为0;接下来i=1,此时R=0,i仍然不在R里,所以看看1位置能扩到哪,我们发现可以扩到2位置,因为 # 1 # 是以1位置为中心的最长回文串 。所以位置1结束后,R变为3;i=2,此时i在R内,这时就可以先不必暴力扩张,先看看能不能加速这个过程,这就是下面要说的情况2:i在最右回文右边界里
-
i在最右回文右边界里,此时又可细分为如下三种情况:
为了便于说明,将最右回文右边界的最早到达点记为C,那么C的最长回文子串的右边界即为R,左边界记为L。
- i关于C的对称点的最长回文串在[L, R]内,例子见下图:
- i关于C的对称点的最长回文串在[L, R]内,例子见下图:
C是最右回文右边界的最早到达点,L与R是以C为中心的最长回文子串的左右边界。对于i位置,由于它在R内,所以不必急着暴力扩张,先看看关于C的对称点i’的回文区域是啥,那i’的回文区域怎么得到呢?用之前求出的最长回文半径长度数组即可得到。上图是i’的回文区域在[L, R]内的情况。此时i的回文区域和i’的相同,为啥呢?证明如下:
先证[r’, l’]为回文:设i’的回文区域是红色范围,左边界为l,右边界为r,r’是r关于C的对称点,l’同理。由于[l, r]是回文,而[r’, l’]是[l, r]的对称点,因此前者是后者的逆序,即[r’, l’]也为回文。
再证i的最大回文区域就是[r’, l’]:y是l的左边一位元素,x是r右边一位元素,x’与y’是x和y关于C的对称点。当时计算i’的回文区域时,为啥没把y和x考虑进去呢?肯定是他俩不相等啊,所以x’和y’也不相等,即i的最大回文区域就是[r’, l’]。
一个例子:字符串zkabatFtabakY,C为F,那么左右边界为:z[kabatFtabak]Y,设i为倒数第四个位置(b),所以i关于C的对称点i’在正数第4个位置(b),而i’的最长回文区域是[aba],在[L, R]里(L在第一个k位置,R在第二个k位置),所以i的回文区域即为[aba],那么i的最长回文半径长度数组的值即为2(aba,半径为ab/ba,故为2)。
- i关于C的对称点的最长回文串在[L, R]外,例子见下图:
此时i关于C的对称点i’的最长回文串超过了C的最长回文串边界L,那么i的最长回文区域也不用算,半径就是[i, R],证明如下:
做L关于i’的对称点L’,R’同理。所以[L, L’]是回文,[R, R]也是回文,且[R, R]是[L, L’]的逆序。
由于C的最长回文区域是[L, R],并不是[X’, X],所以X’不等于X,否则最长回文区域就是[X’, X]了,而X’与Y’关于i’对称且在i’回文区域内,所以X’=Y‘,而Y是Y’关于C的对阵点,所以Y‘=Y,综上可以得出X不等于Y,即这种情况下i的最长回文区域只能是[i, R]。
一个例子,字符串abcdcbatttabcdcF,C为第二个t,那么其最长回文区域是:ab[cdcbat t tabcdc]F,设i到了倒数第三个位置,那么其对称点i’在正数第四个位置,而i‘的最长回文区域为abcdcba,超出了C的最长区域,因此i位置的最长回文为cdc,即回文半径为2。
- i关于 C 的对称点的最长回文串边界正好在L上,例子见下图:
此时i的最长回文区域至少是[i, R],但能否更长还要看R后面的元素都是啥,也就是说这种情况下需要暴力扩张。
总结
Manacher算法可以分为两种大情况,分别是当前索引i在R内和不在R内,若不在R内,直接暴力扩张。
若在R内,根据i的对阵点i’最长回文区域的大小又可分为三种小情况:
- i‘的最长回文区域仍然在C的最长回文区域内,此时i的最长回文区域和i’相同;
- i‘的最长回文区域在C的最长回文区域外,此时i的最长回文区域是[i, R];
- i‘的最长回文区域正好在C的最长回文区域上,此时i的最长回文区域至少是[i, R],能否更长要用暴力扩张试试看。
代码:
public class Manacher {
private char[] manacherString(String str) {
char[] chars = str.toCharArray();
char[] manacherStr = new char[2 * str.length() + 1];
int index = 0;
for (int i = 0; i != manacherStr.length; i++) {
manacherStr[i] = (i & 1) == 0? '#': chars[index++];
}
System.out.println(Arrays.toString(manacherStr));
return manacherStr;
}
public int manacher(String str) {
if (str == null || str.length() == 0) {
return 0;
}
char[] charArr = manacherString(str);
int[] pArr = new int[charArr.length];
int C = -1;
int R = -1;
int max = Integer.MIN_VALUE;
for (int i = 0; i < charArr.length; i++) {
pArr[i] = i < R? Math.min(R - i, pArr[2*C-i]): 1;
while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
if (charArr[i + pArr[i]] == charArr[i - pArr[i]]) {
pArr[i]++;
}else {
break;
}
}
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
max = Math.max(max, pArr[i]);
}
return max;
}
public static void main(String[] args) {
System.out.println(new Manacher().manacher("abcxyzfzyx"));
}
}
对上述代码的说明:
- pArr[i] = i < R? Math.min(R - i, pArr[2*C-i]): 1;这句就是将前面说的大情况小情况都放在一起了,如果i大于等于R,那么最长回文半径数组就先为1,然后暴力扩张;如果i小于R,此时要看R-i更小还是i的对称点i‘的最长回文区域更小,哪个小i的最长回文半径就是哪个,注意,虽然后面紧跟着暴力扩张的代码,但若是无法扩张代码也无法执行,所以虽然R-i更小或i‘的最长回文区域更小都可以直接出答案,但上图代码更简洁。而R-i更小和i‘的最长回文区域相等则仍需要暴力扩张。
练习题 给你一个字符串,让你在末尾添加最少数量的字符,使得添加后的字符串为回文串
关于这道题,可以用Manacher算法的思想来做。
首先找到以给出的字符串最后一个字符结尾的最长回文串,然后从头开始,遇见最长回文串的开头就停止,将这段字串逆序贴在整个字符串的末尾就是答案,举个例子:
- 假设给出的字符串是:abcxyzfzyx,可以看出以最后一个字符结尾的最长回文串是xyzfzyx,然后从字符串的头开始遍历,直到x的前一个位置,最后遍历得到的子串为abc,将abc逆序为cba贴在给出的字符串后面即为答案:abcxyzfzyxcba
只需对Manacher算法做一点改进: 有边界第一次到字符串末尾时就跳出,此时得到的即为以最后一个字符结尾的最长回文串,注意,他不一定是所有回文串中最长的,但这没关系,我们要的是以最后一个字符结尾的最长回文串!!!。
public class Manacher {
private char[] manacherString(String str) {
char[] chars = str.toCharArray();
char[] manacherStr = new char[2 * str.length() + 1];
int index = 0;
for (int i = 0; i != manacherStr.length; i++) {
manacherStr[i] = (i & 1) == 0? '#': chars[index++];
}
System.out.println(Arrays.toString(manacherStr));
return manacherStr;
}
public int manacher(String str) {
if (str == null || str.length() == 0) {
return 0;
}
char[] charArr = manacherString(str);
int[] pArr = new int[charArr.length];
int C = -1;
int R = -1;
int max = Integer.MIN_VALUE;
int earlyC = -1;
for (int i = 0; i < charArr.length; i++) {
pArr[i] = i < R? Math.min(R - i, pArr[2*C-i]): 1;
while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
if (charArr[i + pArr[i]] == charArr[i - pArr[i]]) {
pArr[i]++;
}else {
break;
}
}
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
if (R == charArr.length) {
earlyC = pArr[i];
break;
}
max = Math.max(max, pArr[i]);
}
System.out.println(earlyC);
// 方式1
// char[] res = new char[str.length()-earlyC+1];
// for (int i = 0; i < res.length; i++) {
// res[res.length-1-i] = charArr[i*2+1];
// }
// 方式2
char[] res = new char[(2*C-charArr.length+1)/2];
int index = 0;
for (int i = 2*C-charArr.length; i >=0; i--) {
if ((i & 1) != 0) {
res[index++] = charArr[i];
}
}
System.out.println(String.valueOf(res) + " " + res.length);
return max;
}
public static void main(String[] args) {
new Manacher().manacher("abcxyzfzyx");
}
}
结果:
cba 3