持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
前言
Manacher算法是解决在一个字符串中找最长回文子串的一个算法,并且速度很快!
暴力解
从0位置开始,以每个位置作为中心点往左右两边扩,看能扩到什么位置!但是此方法只适用于回文长度是奇数个的时候。
偶数的话怎么办呢?
用特殊字符把源字符串加工处理一下,比如字符串“121aaaa232aa”就变成
#1#2#1#a#a#a#a#2#3#2#a#a#
在源字符串开始和结束位置加上一个特殊字符,每两个字符之间也插入特殊字符,再从0位置开始,以每个位置为中心点往左右两边扩,看能扩到什么位置!得到结果后除2就是最终结果。(除2是向下取整)。
如果不用特殊字符,用源字符串中的任意一个字符会影响结果吗?
不会!因为源字符只会跟源字符比较,特殊字符只会跟特殊字符比较,没有任何时候,源字符会跟特殊字符比较!!!
暴力解时间复杂度
举一个最差例子:aaaaa,加工后变成
#a#a#a#a#a#
每个位置能扩到的距离分别是0、1、2、3、4、5、4、3、2、1、0,开头到中间和中间到结尾分别都是等差数列,显然,时间复杂度为O(N^2)。
暴力解之所以暴力是因为,每个位置能扩多远这个结果无法指导接下来的位置继续扩充,每个位置都是独立的自己在计算。而Manacher算法就是当前位置的结果可以加速下一个位置的计算。所以跟KMP算法的思想有点类似,都是因为每个位置自己算出来的结果无法指导下一个位置。
回文直径和回文半径
比如字符串
abc12321def
回文直径就是5,回文半径就是3(半径不是直径的一半!!!)
如果回文串长度是偶数个,比如
abc1221def
回文直径就是4,回文半径就是2
如果是加工后的字符串:
#1#2#2#1#
回文直径是9,回文半径是5
回文半径数组
在加工后的字符串的基础上,从左往右求每一个位置为中心向左右两边扩的长度,每一次得到的答案都放到一个回文半径数组parr里面
最右回文边界R
用R表示,一开始设置为-1。表示0位置还没有开始扩,所以就认为右边界在-1位置。
所以最右回文边界表示的就是每一次扩出来的回文区域的最右边。
取得最右回文边界时候的中心点C在什么位置
一开始设置为-1。当最右回文边界更新的时候,C就是负责记录是哪个中心点扩的时候让R更新的。C跟R是伴生的,R不更新C就不会更新;一旦R更新C就会跟着更新。
Manacher算法流程(O(N))
Manacher算法流程跟暴力解的大过程是一样的,也是从0位置开始往左右两边扩,然后从1位置开始,从2位置开始...,但关键是Manacher在扩的时候是有加速的。
注意:Manacher仍然是针对处理过的源字符串!!!
假设以当前来到的 i 位置为中心向左右两边扩,毫无疑问,存在两种情况:
1、i 没有被 R 罩住,此时无法优化,只能暴力扩,看左右两边的字符一不一样,一样就继续阔,不一样就停。 2、i 被 R 罩住了,此种情况又可以细分三种情况。
R 会扩住这个 i,或者正好压着 i,那么中心点 C 一定是在 i 的左边,因为一定是之前某个中心点扩了一个比较远的距离,才把 i 罩住的。那么此时一定能够做出一个关于C的对称点,i' 和 L。
特殊情况,i 和 R 重合,但也存在上述对称关系:
存在上述关系后( i 被 R 罩住的情况),接下来的小情况分类。根据 i' 自己扩出来的回文区域分。i' 是挨着左边的位置,说明之前求过 i' 扩出来的大小,并且把答案存在回文半径数组里面。
1)、i' 扩出来的回文区域彻底在 L..R 内。
举个例子,为了好看,就不加特殊字符了,但是其实应该是针对处理过的字符串的。
此时,i位置能扩的区域一定跟 i' 扩的区域一样大!
证明
2)、i' 扩出来的回文区域跑到 L 外头去了,举个例子:
此种情况下,i 位置能扩多远,i 位置的回文半径就是 i ~ R。
证明
3)i' 扩出来的回文区域的左边界和 L 重合,举个例子:
此种情况下,i 的回文半径大小至少跟 i' 的回文半径大小一样,(这一段无需验证)但是会不会更大?需要接着验证。
Manacher时间复杂度
coding上的细节
// 讲述中:R代表最右的扩成功的位置
// coding:最右的扩成功的位置的,再下一个位置
int R = -1;
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
// pArr[2 * C - i]
// max是加了#字符后的处理串的 回文半径
// max-1就是原字符串的最长回文大小
return max - 1;
完整代码
package com.harrison.class17;
/**
* @author Harrison
* @create 2022-03-30-10:59
* @motto 众里寻他千百度,蓦然回首,那人却在灯火阑珊处。
*/
public class Code01_Manacher {
public static int manacher(String s) {
if (s == null || s.length() == 0) {
return 0;
}
// "12321" -> "#1#2#3#2#1"
char[] str = manacherString(s);
// 回文半径大小
int[] pArr = new int[str.length];
int C = -1;
// 讲述中:R代表最右的扩成功的位置
// coding:最右的扩成功的位置的,再下一个位置
// 只有R>i才表示i被R罩住了!!!
int R = -1;
int max = Integer.MIN_VALUE;
for (int i = 0; i < str.length; i++) {
// R第一个违规的位置,i>=R
// i位置扩出来的答案,i位置扩的区域,至少是多大(也就是说哪个区域是我不用验的)
// Math.min(pArr[2 * C - i], R - i) i'的回文半径 和 R到i的距离谁小谁就是不用验的区域
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
// 继续验证再往外的右 和 再往外的左,如果一样,回文半径大小加1;如果不一样,那就扩失败了
// 但是大情况中的情况1)和情况2)不用验也知道答案,此时进入while就会直接break;
while (i + pArr[i] < str.length && i - pArr[i] > -1) {
if (str[i + pArr[i]] == str[i - pArr[i]]) {
pArr[i]++;
} else {
break;
}
}
// 如果R被推得更往右了,同时跟新R和C
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
// 记录下每一步的回文半径大小
max = Math.max(max, pArr[i]);
}
// max是加了#字符后的处理串的 回文半径
// max-1就是原字符串的最长回文大小
return max - 1;
}
public static char[] manacherString(String s) {
char[] str = s.toCharArray();
char[] res = new char[2 * str.length + 1];
int index = 0;
for (int i = 0; i != res.length; i++) {
res[i] = (i & 1) == 0 ?'#':str[index++];
}
return res;
}
public static int right(String s){
if (s == null || s.length() == 0) {
return 0;
}
char[] str=manacherString(s);
int max=0;
for(int i=0; i<str.length; i++){
int L=i-1;
int R=i+1;
while(L>=0 && R<str.length && str[L]==str[R]){
L--;
R++;
}
max=Math.max(max,R-L-1);
}
return max/2;
}
public static String getRandomString(int possibilities, int size) {
char[] ans = new char[(int) (Math.random() * size) + 1];
for (int i = 0; i < ans.length; i++) {
ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');
}
return String.valueOf(ans);
}
public static void main(String[] args) {
int possibilities = 5;
int strSize = 20;
int testTimes = 5000000;
System.out.println("test begin");
for (int i = 0; i < testTimes; i++) {
String str = getRandomString(possibilities, strSize);
if (manacher(str) != right(str)) {
System.out.println("Oops!");
}
}
System.out.println("test finish");
}
}
本文出自:Manacher算法