Manacher算法
导读
Manacher算法是解决有关回文字符一类的问题。之前寒假看过,琢磨了好久,现在来看,没有那么陌生了,重新理了一遍后,虽然也写了好几遍代码,但感觉已经有是在背代码的感觉了!还是理解万岁。
问题描述
给你一串字符串,返回这个字符串里最长的回文子字符串,或者返回这个最长的回文子字符串的长度。
比如: abccbaaa : 最长回文子字符串为abccba,长度为6
a d s b f p r r p f b l d e : 最长回文子字符串为:bfprrpfb,长度为8
关于什么是回文:
回文就是正着读和反着读的是一样的,或者说他在中间有一个对称轴,关于这个轴两边是镜像的。
顺便贴张图片,前几天在LeetCode上看到的,震惊到我了。
思路
普通解法:
对于字符串中的每个字符,都以他为中心,向两边扩来检查,遇到不相等的就停下,在这个过程中,记录这个最大长度和相对应的字符。
这样对于 奇数个的字符串 还可以用,但对于 偶数个的字符串 就不行了。举个例子:aba,可以找出答案, abba,依次为中心往外扩,得到的答案不对。
所以,这里我们就要对原来的字符串做一些预处理:在每个字符之间插入相同的字符,可以是#,可以是%,甚至可以是原字符串里的字符都可以,这个无所谓,这是起一个辅助作用,帮助我们占位置,是字符都变成 奇数个。
Manacher:
在介绍该算法之前,先介绍需要用到的一些变量:
pArr : 一个整型数组,长度和经过处理后的字符串的长度相同,每个位置上代表的意思是 在字符串里,以该位置为中心的话,他的最长回文的半径是多长,注意是半径,不是直径。
pR : 一个整型变量, 意思是目前为值,可以扩到的最长回文半径的右边界,因为这样的话,在pR以左的部分我们都可以利用已知信息来加速这个扩充过程。
center: 一个整型变量, 那个最长回文右边界pR对应着 回文中心,也就是 以center为中心,可以扩到pR长度。
好了,下面开始实施Manacher算法了:
假设我们已经遍历到了i位置,i位置有2中可能
第1中可能: i 在pR的右侧,这样Manacher派不上用场,直接暴力扩就好。
第2中可能: i 在pR的左侧。 这种可能又分为3种小情况。这个过程就是Manacher的核心思想了。
第①: i 关于 center 的对称点 i一撇, i一撇的回文半径我们之前已经算出来了,如果他的回文半径在pR的范围里,那么 i 的回文半径 和 i 一撇的回文半径相同。看着示意图应该就明白了。
(这个示意图我懒得分就贴到一起啦…)
第②; i 关于 center 的对称点 i一撇, i一撇的回文半径我们之前已经算出来了,如果他的回文半径压着pR的范围左边界, 那么i 的回文半径是从pR往右扩的,i-pR的范围可以确定是回文了。 也是看示意图。
第③:i 关于 center 的对称点 i一撇, i一撇的回文半径我们之前已经算出来了,如果他的回文半径已经超过了pR的范围,那个 i 的回文半径长度最长就是到pR了,因为如果比这个长的话,那么原来的pR就不会是现在这个值了,与我们之前定义好的,进而求出来的pR相矛盾。 也是看看示意图。
代码实现
虽然情况是分很多种的,但是写成代码后还是比较简洁的,还是像最前面写的那样,理解就好。
// 对原字符串 进行 预处理
public static char[] manacherString(char[] chs) {
char[] chsA = new char[chs.length*2+1];
int index = 0;
for(int i = 0; i < chsA.length; i++) {
chsA[i] = i % 2 == 0 ? '#' : chs[index++];
}
return chsA;
}
// Manacher算法
public static int manacher(String s) {
if(s == null || s.length() == 0) {
return 0;
}
char[] charArr = manacherString(s.toCharArray());
int pR = -1;
int center = -1;
int[] pArr = new int[charArr.length];
int len = Integer.MIN_VALUE;
for(int i = 0; i < charArr.length; i++) {
pArr[i] = i < pR ? Math.min(pR-i, pArr[center*2-i]) : 1;
while(i+pArr[i] < charArr.length && i-pArr[i] >= 0) {
if(charArr[ i+pArr[i] ] == charArr[ i-pArr[i] ] ) {
pArr[i]++;
}else {
break;
}
}
if(i + pArr[i] > pR) {
pR = i + pArr[i];
center = i;
}
len = Math.max(len, pArr[i]);
}
return len-1;
}
小应用
给你一个字符串,要求只能在他的末尾添加字符,使他整体构成一个回文字符串。
思路:
改写Manacher算法,该问题就是求必须包含最后一位字符的最长回文字符串是什么(也就是pR到最后一个位置时就停止了),在吧剩下的字符逆序过来 再 添加到末尾即可。
代码:
// 对原字符串 进行 预处理
public static char[] manacherString(char[] chs) {
char[] chsA = new char[chs.length*2+1];
int index = 0;
for(int i = 0; i < chsA.length; i++) {
chsA[i] = i % 2 == 0 ? '#' : chs[index++];
}
return chsA;
}
// 返回的是添加的那部分字符
public static String manacherAd(String s) {
if(s == null || s.length() == 0) {
return null;
}
char[] charArr = manacherString(s.toCharArray());
int center = -1;
int pR = -1;
int[] pArr = new int[charArr.length];
int maxContainsEnd = 0; // 包含最后一位字符对应的回文是有多长
for(int i = 0; i < pArr.length; i++) {
pArr[i] = i < pR ? Math.min(pR-i, pArr[2*center-i]) : i;
while(i+pArr[i] < charArr.length && i-pArr[i] >= 0) {
// 保证扩的时候不越界
if(charArr[ i+pArr[i] ] == charArr[ i-pArr[i] ] ) {
pArr[i]++;
}else {
break;
}
}
if(i+pArr[i] > pR) {
pR = i + pArr[i];
center = i;
}
if(pR == charArr.length) {
maxContainsEnd = pArr[i];
break;
}
}
char[] res = new char[ s.length()-maxContainsEnd+1 ];
for(int i = 0; i < res.length; i++) {
res[res.length-1-i] = charArr[i*2+1];
}
return String.valueOf(res);
}