Manacher
题目:求一个字符串中最长的回文串的长度。
如果将字符串化成数组,每遍历一个元素,都要以其为中心判断回文串的长度,时间复杂度比较大。于是就有了Manacher算法,它和KMP算法一样,都是在暴力解的基础上进行改进。
数据处理
对于一个长度为奇数回文字符串,按照暴力法能够得出正确结果。
但是对于长度为偶数的回文字符串,按暴力解法是不能得出正确结果的。
因为长度为偶数的回文字符串的对称轴是虚的,如:aa
、abba
。而长度为奇数的回文字符串的对称轴是字符串中某个具体的元素。
为了解决这个问题,可以将字符串转化,往其中添加辅助字符。如#a#a#
,#a#b#b#a#
。这样的话,无论给定字符串的长度是多少,都能正常地进行判断。(用任何字符填充都行,比如说填充了’#’,无论以谁为对称轴,比较的过程中,’#‘只会和’#‘比较,而实际给定的字符不会和’#'比较,不会影响最终结果。)
处理字符串的代码如下
public static char[] change(String str){
//将字符串转换为数组
char[] arr = str.toCharArray();
int len = arr.length;
//构造一个数组,使用"#"填充
char[] arr2 = new char[2 * len + 1];
arr2[0] = '#';
for(int i = 1, j = 0; j < len; i++, j++){
arr2[i] = arr[j];
arr2[++i] = '#';
}
return arr2;
}
算法思路
首先引入几个变量(这几个变量的规则是自己定的,不是官方定义。)
-
回文半径数组:遍历字符串求该数组
例如
aba
,以第一个字符a
为中心,它自己构成一个回文,记半径为1;以b
字符为中心,可构成回文aba
,故记半径为2。第三个字符a
的回文半径为1。则字符串
aba
对应的回文半径数组为{1,2,1}
。 -
最大的回文右边界R(下标):初始值为-1
例如
aba
,从头开始遍历,当遍历到第一个字符a
时,它自己构成回文,此时回文右边界为字符b
的下标,即为1。例如
sabtbamn
,从头开始遍历,当遍历到字符t
时,它构成回文abtba
,此时回文右边界为字符m
的下标,即为6。继续向后遍历到第二个b
字符,他自身构成回文,但此时最大回文右边界仍为6。 -
最大的回文右边界对应的中心C(下标):初始值为-1,随R的值改变而改变。
例如
sabtbamn
,从头开始遍历,当遍历到字符t
时,它构成回文abtba
,此时最大回文右边界为字符m
的下标,即为6。而此时中心C就是字符t
对应的下标3。继续向后遍历到第二个b
字符,最大回文右边界没有变,则C的值也不变,仍为3。
然后介绍数组遍历过程中会出现的集中情况。
-
遍历指针i >= R
如下图,当i=4时,需要以
m
为中心,向两边扩散判断i=4时的回文半径
-
遍历指针i < R,且i关于C的对称点i’的回文半径小于(R-i),如下图所示
上图中i=8,其关于C的对称点为i’,因为已经遍历到8,所以i’的回文半径已知。且该回文半径小于(R-i),于是i的回文半径可以直接得出来,与**i’**的回文半径相同。 -
遍历指针i < R,且i关于C的对称点i’的回文半径大于(R-i),如下图所示
此时i=8,最大的回文右边界R=10,最大的回文右边界对应的回文串为abastsaba
。i关于C的对称点i’的回文半径大于(R-i),此时要判断i的回文半径,需要判断arr[6]
是否与arr[10]
相等…等后续判断对应的代码如下
public static int function2(String str){ //转换数组 char[] charArr = change(str); int[] pArr = new int[charArr.length];//charArr[i]的回文半径 int C = -1;//回文右边界最大时,此回文的中心 int R = -1;//最大的回文右边界 int max = Integer.MIN_VALUE; for(int i = 0; i != charArr.length; i++){ if(i < R && pArr[2 * C - i] < R - i){//第二种情况 pArr[i] = pArr[2 * C - i]; }else{ if(i >= R){//第一种情况 pArr[i] = 1;//pArr[i]的回文半径最小为1,需要接着判断 }else{//第三种情况 pArr[i] = R - i;//pArr[i]的回文半径最小为R--i,需要接着判断 } //对以上两种情况接着判断,看回文半径是否还能增大 while(i + pArr[i] < charArr.length && i - pArr[i] > -1){ if(charArr[i + pArr[i]] == charArr[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; }
其实还可以使用更简洁的代码,上面的代码比较容易理解
public static int function2(String str){ //转换数组 char[] charArr = change(str); int[] pArr = new int[charArr.length];//charArr[i]的回文半径 int C = -1;//回文右边界最大时,此回文的中心 int R = -1;//最大的回文右边界 int max = Integer.MIN_VALUE; for(int i = 0; i != charArr.length; i++){ //如果i>=R,先将其回文半径设为1,后续还要继续判断回文半径是否更大 //(2 * C - i)为i关于C的对称点i' //如果i<R,且i'的回文半径<(R-i),则i的回文半径等于i'的回文半径,后续的while循环直接退出 //如果i<R,且i'的回文半径>=(R-i),暂时将i的回文半径设为(R-i), 有可能i的回文半径会超过R与i之间的距离,后续还要继续判断 pArr[i] = i < R ? Math.min(pArr[2 * C - i], R - 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; } } //更新R和C if(i + pArr[i] > R){ R = i + pArr[i]; C = i; } max = Math.max(max, pArr[i]); } return max - 1; }