在之前的文章中,我分析了最长回文子序列的问题,是比较通用的方法,即将最长回文子序列问题转化为我们最常见的LCS问题:通过求字符串本身及其逆序字符串的LCS问题即可求得最长回文子序列问题,时间复杂度为O(n^2),这对于可不连续的子序列是一个通用解法。那么,对于回文子串即结果是连续的回文子序列是否有更好的解法呢?答案当然是有啊,就是我们今天要讲的主角,马拉车算法(Manacher),这与普通的解法最大的改进,便是引入了变量保存回文串的最大右边界,并用一个数组保存以每个字符为中心轴的回文半径。
首先,为了避免计算回文半径因为奇偶数的问题要分类讨论,则在原字符串中字符之间插入分隔符,如‘#’。这样就可以将奇数回文串与偶数回文串统一讨论了,然后建立辅助数组rad[],用rad[i]表示第i个字符的回文半径,rad[i]至少为1,即其本身。假设已知rad[0]~rad[i]的值,然后求i后面的rad值,所以该算法的重点就在这里:
图1
如图1所示:rad[i] - k < rad[i - k];rad[i - k]的范围为青色。因为黑色的部分是回文的,且青色的部分超过了黑色的部分,所以rad[i + k]肯定至少为rad[i]-k,即橙色的部分。那橙色以外的部分就不是了吗?这是肯定的,因为如果橙色以外的部分也是回文的,那么根据青色和红色部分的关系,可以证明黑色部分再往外延伸一点也是一个回文子串,这肯定是不可能的,因此rad[i + k] = rad[i] - k。
图2
如图2所示:rad[i] - k > rad[i - k],rad[i-k]的范围为青色,因为黑色的部分是回文的,且青色的部分在黑色的部分里面,根据定义,很容易得出:rad[i + k] = rad[i - k]。根据上面两种情况,可以得出结论:当rad[i] - k != rad[i - k]的时候,rad[i + k] = min(rad[i] - k, rad[i - k])。
图3
如图3所示,rad[i] - k = rad[i - k],通过和第一种情况对比之后会发现,因为青色的部分没有超出黑色的部分,所以即使橙色的部分全等,也无法像第一种情况一样引出矛盾,因此橙色的部分是有可能全等的。但是,根据已知的信息,我们不知道橙色的部分是多长,因此就需要再去尝试和判断了。
通过对以上三种情况的讨论,可以知道我们在求的过程中需要用两个变量分别保存回文串最大右边界right,以及其对应的回文中心id,并动态更新。遍历i的过程中,需要求每个i对应的半径,初始化半径r=1.如果i在最大右边界之内,则根据已知的半径更新新半径,避免重复计算。而半径大小取决于两个值,一个是最大右边界对应的半径减去id到i的差值,即r1=rad[id]-(i-id),因为i在边界之内,则r1大于0;另一个值则找到i关于最大回文中心的对称点, 该点由于在中心点左边是已知的,直接查询,r2=rad[id-(i-id)]。根据上述的三种分析情况,r=min(r1,r2)。然后根据求得的半径并对半径进行扩大(要求i-r和i+r在边界范围内),若i-r和i+r处对应字符相等,则扩大半径。接着需要更新由边界以及对应的中心点id。如此重复,直到遍历结束。rad数组中最大值减1就是最大回文串半径。如果需要求得具体的最大回文子串,则需要求得最大半径对应的下标,并相结合求得相应结果。
有了以上的知识储备,我开始讲讲51nod上刷到的一道算法题吧,如下图所示:
上图所示的题目考虑到str的长度上限为100000,若使用一般方法,时间复杂度是平方级别的,则必定超时,所以此时应该使用马拉车算法,时间属于线性级别,符合题目的时间要求。
代码实现:
package manacher;
import java.util.*;
public class Manacher {
public static int getPalindromeLength(String str) {
// 1.构造新的字符串
// 为了避免奇数回文和偶数回文的不同处理问题,在原字符串中插入'#',将所有回文变成奇数回文
StringBuilder newStr = new StringBuilder();
newStr.append('#');
for (int i = 0; i < str.length(); i ++) {
newStr.append(str.charAt(i));
newStr.append('#');
}
// rad[i]表示以i为中心的回文的最大半径,i至少为1,即该字符本身
int [] rad = new int[newStr.length()];
// right表示已知的回文中,最右的边界的坐标
int right = -1;
// id表示已知的回文中,拥有最右边界的回文的中点坐标
int id = -1;
// 2.计算所有的rad
// 这个算法是O(n)的,因为right只会随着里层while的迭代而增长,不会减少。
for (int i = 0; i < newStr.length(); i ++) {
// 2.1.确定一个最小的半径
int r = 1;
if (i <= right) {
r = Math.min(rad[id] - i + id, rad[2 * id - i]);
}
// 2.2.尝试更大的半径
while (i - r >= 0 && i + r < newStr.length() && newStr.charAt(i - r) == newStr.charAt(i + r)) {
r++;
}
// 2.3.更新边界和回文中心坐标
if (i + r - 1> right) {
right = i + r - 1;
id = i;
}
rad[i] = r;
}
// 3.扫描一遍rad数组,找出最大的半径
int maxLength = 0;
int index=0;
for (int i=0;i<rad.length;i++) {
if (rad[i] > maxLength) {
maxLength = rad[i];
index=i;
}
}
System.out.print("最大回文子串是:");
for(int j=index-maxLength+1;j<index+maxLength;j++)
{
if(newStr.charAt(j)!='#')
System.out.print(newStr.charAt(j));
}
System.out.println();
System.out.print("最大回文子串长度是:");
return maxLength - 1;
}
public static void main(String[] args)
{
Scanner sc=new Scanner(System.in);
String s=sc.nextLine();
int n=s.length();
System.out.print(getPalindromeLength(s));
}
}
测试结果:
daabaac
最大回文子串是:aabaa
最大回文子串长度是:5
我经过了两天时间琢磨马拉车算法,阅读了不少相关的资料,现在终于将该算法琢磨清楚,期间最感谢的还是下面这篇博客,给了我很大帮助,我也借鉴了不少技巧。
参考博客:http://blog.sina.com.cn/s/blog_3fe961ae0101iwc2.html
转发请注明:转自http://blog.csdn.net/carson0408/article/details/78765106