算法简介
Manacher算法是一种用来求给定字符串的最大回文子串长度的算法。它相对于简单的中心扩展算法的优势在于不用从头开始扩展,也就是说计算以索引值为i的字符为中心的回文子串的长度时,不必从i-1和i+1开始比较,而是可以利用回文子串本身的一些性质,从离位置i更远的两头开始扩展,减少计算量。下面我们具体地看一下这个算法。
算法过程
我们目前只考虑所有位置的最大回文子串的长度是奇数的情况,偶数的情况也可以被处理成奇数来计算。
我们用数组p来记录每一个位置的最大回文子串的长度,也就是p[i]的值是以位置i的字符为中心的最大回文子串的长度。在下面的行文中,为了方便,也用p[i]来表示以i位置为中心的最大回文子串,具体的含义请根据上下文来理解。我们从i等于0开始执行算法,执行到i等于字符串长度-1为止,数组p里面的最大值就是最大回文子串长度。
再定义几个变量,c,l,r。他们分别表示当前遇到的最大回文子串的中心位置,当前遇到的最大回文子串的最左侧位置,当前遇到的最大回文子串的最右侧位置。如果遇到更长的回文子串,则更新c,l,r的值。
在上图中,我们的算法执行到i等于4了,此时,c为3,l为1,r为5。
我们该计算p[4]的值了,我们在扩展之前,先看看现在可以利用哪些已知信息来避免从索引为4的位置开始扩展,如果我们可以从更远的地方扩展,我们就可以减少计算量。
这里可以利用的信息为:
当i小于r时,肯定存在一个位置j,j和i对称于c,j=(2 * c) - i(因为i-c = c-j)。
j和i是关于c对称的,所以j的最大回文子串和i的最大回文子串是关于c部分对称的,我们现在要做的,就是把他们的对称部分找出来,跳过对称部分,再找出i的最大回文子串。
这里的关键就是p[j]我们已经知道了,也就是j位置的最大回文子串长度我们已经知道了,但是不能直接p[i] = p[j],因为j的最大回文子串的左边界有可能小于l,这时候如果r右边的部分和l左边的部分关于c对称,那么l和r就不是以c为中心的最大子串的左右界了。p[i]和p[j]关于c对称的部分的右边界不能超过r,p[i]的中心位置是i,所以p[i]和p[j]关于c对称的部分的最大长度是r-i。当p[j]<r-i时,p[i]=p[j],否则,p[i]=r-i。写成代码就是:
if(i < r){
P[i] = Math.min(r - i, P[j]);
}
接下来的事情就好办了,从i + (1 + P[i])和i - (1 + P[i])处向两头扩展,找出i位置的最大回文子串长度。
算法的Java参考实现
int manachersAlgorithm(String s, int N) {
String str = getModifiedString(s, N);
int len = (2 * N) + 1;
int[] P = new int[len];
int c = 0;
int r = 0;
int maxLen = 0;
for(int i = 0; i < len; i++) {
int mirror = (2 * c) - i;
if(i < r) {
P[i] = Math.min(r - i, P[mirror]);
}
int a = i + (1 + P[i]);
int b = i - (1 + P[i]);
while(a < len && b >= 0 && str.charAt(a) == str.charAt(b)) {
P[i]++;
a++;
b--;
}
// 如果超过r,则更新c,r的值
if(i + P[i] > r) {
c = i;
r = i + P[i];
if(P[i] > maxLen) { //update maxlen
maxLen = P[i];
}
}
}
return maxLen;
}
/**
* 在字符串中插入特殊字符,让回文子串长度为奇数
* @param s 原始字符串
* @param N 原始字符串长度
* @return 处理后的字符串
*/
String getModifiedString(String s, int N){
StringBuilder sb = new StringBuilder();
for(int i = 0; i < N; i++){
sb.append("#");
sb.append(s.charAt(i));
}
sb.append("#");
return sb.toString();
}