资料来源网络 参见:http://www.felix021.com/blog/read.php?2040
求解字符串的回文问题的时候,如果不对字符串做一些处理,我们会遇到回文子串的长度是奇数或者偶数的分类处理。
但是,采用Manacher算法可以完全避免这个问题。
Manacher算法:在原来字符串的头、尾以及字符之间都添加一个从来没有出现过的字符,作为分隔符。例如:”#”。
就可以把奇数和偶数区别问题完全转化为奇数问题。
具体做法:在每个字符的两边都插入一个特殊的符号。
比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 由于我采用java语言,就免去避免越界的额外添加操作。
下面以字符串12212321为例,经过上一步,变成了 S[] = “$#1#2#2#1#2#3#2#1#”;
用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:
S # 1 # 2 # 2 # 1 # 2 # 3 # 2 # 1 #
P 1 2 1 2 5 2 1 4 1 2 1 6 1 2 1 2 1
由于L=2*P[i] -1,是新字符串中以S[i]为中心的回文子串的最长长度;
新字符串都是以”#”开始或者结束,原字符串的回文长度为:(L-1)/2;
由上面两个式子可以得到原字符串回文长度为:P[i]-1。
现在的问题是:如何求解P[i]数组的值?
该算法增加两个辅助变量(其实一个就够了,两个更清晰)id和mx,其中 id 为已知的 {右边界最大} 的回文子串的中心,mx则为id+P[id],也就是这个子串的右边界。
然后可以得到一个非常神奇的结论,这个算法的关键点就在这里了:如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。就是这个串卡了我非常久。实际上如果把它写得复杂一点,理解起来会简单很多:
//记j = 2 * id - i,也就是说 j 是 i 关于 id 的对称点(j = id - (i - id))
if (mx - i > P[j])
P[i] = P[j];
else /* P[j] >= mx - i */
P[i] = mx - i; // P[i] >= mx - i,取最小值,之后再匹配更新
当然光看代码还是不够清晰,还是借助图来理解比较容易。
当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图。
当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。
对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。
反思
个人在这里卡了很长时间,并且刚刚开始都不小这句话是来干嘛的。
如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。
主要是经常做某公司的题目,动不动就用dp,所以一开始用的dp做的,但是提交以后表示:超时无解。
代码如下:
import java.util.Scanner;
/**
* 动态规划问题
*
* @author luopan
*
*/
public class Mian {
/**
* 返回有效密码串的最大长度
*
* 求解最长对称子串
*
* 求最长回文
*
* @param str
* @return
*/
private static int getMaxLength(String str) {
int len = str.length();
int maxLen = 0;
// 1、声明dp二维数组记录[i-j]之间是不是回文
int dp[][] = new int[len][len];
// 2、初始化dp数组,并且每个字符串本身都是回文,设置为1,表示是
for (int i = 0; i < len; i++) {
dp[i][i] = 1;
}
// 3、初始化dp数组,并且相连的两个字符串相等的也是回文,设置为1,表示是
for (int i = 0; i < len - 1; i++) {
if (str.charAt(i) == str.charAt(i + 1)) {
dp[i][i + 1] = 1;
}
}
// 4、计算以某个字符为中心,左右发散比较是否相等
// 外层循环l表示包括回文中心的总长度,从只有一个左和一个右开始循环,所以是3
for (int l = 3; l <= len; l++) {
for (int i = 0; i <= len - l; i++) {
int j = i + l - 1;
if (str.charAt(i) == str.charAt(j)) {
// 同时向左向右发散
// i在i+1的左边
// j在j-1的右边
dp[i][j] = dp[i + 1][j - 1];
if (dp[i][j] == 1 && l > maxLen) {
maxLen = (j + 1) - i;
// String longStr = str.substring(i, j+1);
}
} else {
dp[i][j] = 0;
}
}
}
return maxLen;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.nextLine().trim();
System.out.println(getMaxLength(str));
}
scanner.close();
}
}
对比着看了一下,Manacher算法保存了之前计算好的值,而我自己写的dp没有保存,每次都要重新计算,严重加长了运行时间。