Manacher算法
问题场景
假设字符串str长度为N,想返回最长回文子串的长度
回文即是指字符串具有对称性,从头到尾遍历和从尾到头遍历结果相同 如“123321” 或 “abc1cba”就是典型的回文子串
解决思路
前置知识
暴力解决的方式,就是从字符串str的第一个元素开始,向两边扩张,到第i个元素时,如果str[i-1] == str[i+1] 那么说明符合回文条件,可以继续扩张。这么做的问题是,可以解决“12321”这种字符串,但是遇到“123321”这种中间是两个数的就会出问题。
为了解决上述问题,初步的解决方式是在元素的空档加入辅助元素,例如可以这样来处理,将原有的字符串"123321"变为"#1#2#3#3#2#1#"
//"123321" -> "#1#2#3#3#2#1#"
public static char[] manacherString(String str) {
char[] charArr = str.toCharArray();//字符串变字符数组
char[] res = new char[str.length() * 2 + 1];//定义新字符串的字符数组 长度为2N+1
int index = 0;
for (int i = 0; i != res.length; i++) {//移动新数组下标
//新数组中插元素 下标奇数插'#',下标偶数插原有数组元素并移动原有数组下标
res[i] = (i & 1) == 0 ? '#' : charArr[index++];
}
return res;
}
这样通过之前的扩张思路处理manacherString方法得到的新数组,将会得到包括"#"在内的最大回文子串,时间复杂度为O(N2)(因为最坏的情况每个元素都需要扩张到最左边或者最右边,最终会得到一个等差数列的求和,复杂度为N2)
Manacher算法的引出
在之前暴力解决的过程中,其实不难发现,在比较过程中,有一些操作是重复进行的,比如在字符串"aba23232aba"中共有11个元素,其中下标为1的“b”和下标为9的“b"的最大回文子串长度是一样的,但是暴力解决时会不分青红皂白开始扩张,显然在浪费资源,那么有更好的方法吗?
几个概念
回文半径 :”123321“ 回文半径为3 ”12321“ 回文半径为3
( 看一眼就懂的概念,感觉比文字介绍好很多)
回文半径数组pArr
就是把每个元素的回文半径存到数组里
回文最右边界R,中心点C
比如经扩张处理后,得到的最长回文子串是“#1#2#2#1#”,R就是最右边的#,也就是str[8],C点就在两个2中间的“#”,即str[4]
当前元素i ,以及对称元素i` 两元素关于中心点C对称
条件划分
-
i在R外 暴力扩张
-
i在R内(包括元素 i 的下标与 R 的下标相同) 此时元素 i 在中心点C右边,中心点C左边必有相同元素 i’ ,根据 i’ 的回文区域细分
-
i’ 回文区域在左边界L之内
元素 i 的回文区域与 i’ 的回文区域完全相同
-
i’ 回文区域在左边界L之外
元素 i 的回文区域右边界与当前回文最右边界R重合
-
i’ 回文区域左边界与当前回文最左边界L重合 即压线的情况
元素 i 的回文区域从当前回文最右边界R开始继续向右扩张
-
代码实现
public static int manacher(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] str = manacherString(s);//"12132" -> "#1#2#1#3#2#"
int[] pArr = new int[str.length];// 定义回文半径数组 用于统计各元素的回文半径
int C = -1;//中心点 起始点为-1 回文右边界R更新 C就更新为当前回文区域的中心点
int R = -1;// 定义回文最右边界 初始值为-1 含义是扩张成功的最右边界的下个位置
int max = Integer.MIN_VALUE;
for (int i = 0; i != str.length; i++) {// i位置扩出来的答案,i位置扩的区域,至少是多大。
//R < i时 即i在R外或者压线,回文半径为1 即只包含自身 继续暴力扩张
//R == i 时,回文半径为1,因为i跟R压线,无论i'回文区域如何,i回文区域均为自身或者从R开始扩张
//R > i时,即i在R内,回文半径取对称点i'的回文半径,即i'回文区域在左边界L之内或取R-i,即i’回文区域在左边界压线和最左边界L重合,i回文区域从R开始继续向右扩张或者是i'回文区域在左边界L之外,即i的回文区域只能到R
//取min的原因就是i’回文区域在L内时 pArr[i’] < L-i' = R-i
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
while (i + pArr[i] < str.length && i - pArr[i] > -1) {
//判断是否满足回文条件 进行扩张
if (str[i + pArr[i]] == str[i - pArr[i]])
pArr[i]++;
else {
break;
}
}
//更新R 更新C
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
max = Math.max(max, pArr[i]);//比较得到最大回文子串的回文半径
}
return max - 1;//由于有特殊符号#的原因,最大回文子串的回文半径 - 1 就是原字符串的最大回文子串长度
}
PS:关于条件划分的一些图,因为是手工画的原因,较丑,就不往上粘了