马拉车算法主要解决的是找到一个字符串中最长的回文子串。
暴力解
暴力解的思路是遍历字符串str中的每一个字符str[i],从str[i]开始往两边扩,记录下每个以str[i]为中心的回文长度。这种方式的时间复杂度为O(n*n)。
即使是以这种方法,奇回文还能求解,偶回文便不能直接扩,否则会出错。例如“abba”,以str[0]为中心的回文长度为1,以str[1]为中心的回文长度为1,以str[2]为中心的回文长度为1,以str[3]为中心的回文长度为1,最终最长的回文子串为1,很显然出错了。
需要一个技巧,把字符串中所有相邻字母间和头尾都加一个“#”,例如:“abba”转为“#a#b#b#a#”,而“abbba”转为“#a#b#b#b#a#”,这样都转为奇数,扩的时候还是一样的扩法,奇回文和偶回文最后的答案统一都是最终得到的最长回文长度/2。
Manacher算法
先将一些需要准备的工具或概念:
- help[]数组,里面记录每个位置的回文半径,用以之后加速计算;
- 回文半径右边界:在所有的回文中最靠右边的边界位置R;
- 回文右边界的中心:在所有的回文中最靠右边的边界位置R对应的回文中心C。
下面就讲一下计算help[]数组的流程:在计算任意位置i时,可能出现的情况:
(先提一下变量概念:R为到目前位置的回文右边界,C为相应的中心,i’为i以C为中心的对称位置)
- 若位置i在回文右边界R外,暴力计算以位置i为中心的回文半径;
- 若位置i在回文右边界R内,
- i’位置的回文左边界(由于help[i’]已知,故i’的回文左边界已知)在C位置的回文左边界内:i的回文半径与i’相同,不用扩;O(1)
- i’位置的回文左边界在C位置的回文左边界外:i的回文半径为i到R的距离;O(1)
- i’位置的回文左边界与C位置的回文左边界相同:从i到R是不用检验的,在这范围内必是回文,但是能不能在往外扩我们未知,需要从R开始往外扩去暴力尝试。
以上就讲完了所有可能性,其实画张图会比较清晰,由于时间原因,就不画了。。有了计算流程就可以写代码啦。
贴代码:
public static int manacher(String str) {
if (str.length() == 0) return 0;
String s = transfer(str);
int C = -1;//记录回文右边界的中心
int R = -1;//记录回文右边界,开区间
int max = Integer.MIN_VALUE;
int[] arr = new int[s.length()];//回文半径数组
for (int i = 0; i < arr.length; i++) {
arr[i] = R > i ? Math.min(arr[2 * C - i], R - i) : 1;
while (i + arr[i] < arr.length && i - arr[i] > -1) {
if (s.charAt(i + arr[i]) == s.charAt(i - arr[i])) {
arr[i]++;
} else {
break;
}
}
if (i + arr[i] > R) {
R = i + arr[i];
C = i;
}
max = Math.max(max, arr[i]);
}
return max - 1;
}
public static String transfer(String str) {
if (str.length() == 0) return str;
StringBuilder sb = new StringBuilder("#");
for (int i = 0; i < str.length(); i++) {
sb.append(str.charAt(i));
sb.append("#");
}
return String.valueOf(sb);
}
由于回文右边界R只能向右扩不会往回走,所以时间复杂度为O(n)。
相关题目
求在一个字符串末尾添加最少的字符数使的整个字符串为回文串。
解法:求包含字符串原串中最后一个字符的回文直径,前面不包含在回文直径中的字符将其逆序在字符串末尾就是要添加的最少字符。
例如:字符串“abc12321”,用马拉车算法求包含末尾字符“1”的回文直径,即“12321”,前面不包含的的字符串“abc”将其逆序添加到原串末尾为“abc12321cba”即为答案。