字符串的另一个算法:Manacher‘s Algorithm,是一个名为Manacher的人发明的一个关于最长回文子串的算法,称为Manacher算法,俗称马拉车算法。
最长回文串(Longest palindromic substring)是指的在一个字符串中查找最长的回文子串,回文串(palindromic string)是指这个字符串无论从左读还是从右读,所读的顺序是一样的;也就是说,回文串是左右对称的。例如:level、ana等等。
最简单的方法就是暴力穷举法,从字符串的第一个字符到最后一个字符,分别对从该字符开始的所有长度的子串进行判断是否是回文串(该子串的第一个和最后一个字符是否相等、第2个和倒数第2个是否相等。。),代码大致如下:
判断一个子串是否是回文串的代码:
mid = len(s) / 2
last = len(s) - 1
for (i= 0; i < mid; i++) {
if (s[i] != s[last - i]) return false;
}
主程序代码:
for( i = 0; i < last; i++) {
for (j= i + 1; j <= last; j++) {
//调用判断子串是否是回文串的函数。。。。。。
longest = s[i:j+1] }
}
显然,这个程序的时间复杂度是O(n3),效率很低。
Manacher在1975年在ACM上发表了一篇论文:Manacher, Glenn (1975), “A new linear-time “on-line” algorithm for finding the smallest initial palindrome of a string”, Journal of the ACM, 22 (3): 346–351, doi:10.1145/321892.321896,给出了Manacher算法,这个算法可以在线性时间复杂度的情况下找出最长回文子串。
关于Manacher和他的算法,我在wikipedia上查到了相关的介绍,还有他的这篇文章的链接,可惜要从ACM上下载他的这篇文章需要15$$(ACM的会员是10刀)。
一
在这个算法中,首先要对字符串做一个简单处理,把它的长度统一变成奇数,以方便后续处理。因为偶数长度的回文“deed”和奇数长度的回文“level”处理起来不太一样,所以最好是都统一成奇数长度,方法是给原字符串中每一个字符的左右都加上一个特殊字符如“#”:
“deed” --> “#d#e#e#d#”,“level” --> “#l#e#v#e#l#”
道理很简单,无论原字符串长度是奇数偶数,首尾都各加了一个“#”,另外,原长度是偶数的话,它们之间有奇数个空,可以加入奇数个“#”。同理原长度是奇数的话,它们之间有偶数个空可以加入偶数个“#”,这样就都变成了奇数长度,而且这样做并不改变原字符串中的回文。
当字符串长度都变为奇数时,回文串就会有一个中心对称轴,就是回文中间那个字符,他的两边都是对称相等的,这个对称相等的两边的字符长度也可以叫做半径。
二
第二步就是要对这个长度变为奇数的字符串s[],生成一个对应的数组p[ ]。对于每一个字符s[i],对应的p[i]是一个整数值,这个值代表在字符串s中,以字符s[i]为中心,它的回文半径的长度,如果p[i]=1,表示回文只是s[i]自己。
看一个例子:
原串是“abbabb”,s[]是加入“#”后的串“#a#b#b#a#b#b#”,则p[]=“1212521612321”
显然,p[i]中最大的是p[8]=6,对应的回文中心是“a”,回文半径长度6,对应最长的回文是“#b#b#a#b#b#”,对应的原字符串回文是“bbabb”,最大回文长度为5。
可以推导出,当p[i]为最大回文半径时,对应的原字符串中最大回文长度为( p[i]-1 )。
显然,如果有了数组p[],只要找出最大的p[i],那么原串中的最大回文长度就是p[i]-1。
那么,如何计算出数组p[]呢?这是这个算法的关键。
假设,有一个字符串s[],我们现在要计算p[i],p[i]以前的每一个p值都已经计算出来了。而且在之前的计算过程中,对于第id个字符,它的回文子串最远达到了mx这个位置。
在这里,我们要分为“mx>i” 和 “mx<i” 两种情况,并且在“mx>i”这种情况下,还要再分为两种情况分别考虑。
2.1 当(mx>i)时,再分为两种情况
当 mx>i时,则 p[i]=MIN( p[2*id-i], mx-i)。写成代码就是:
if( mx > i)
p[i]=MIN( p[2*id-i], mx-i)
首先,以id为中心,回文对称半径时mx,id左边长度为mx个字符(含第id个字符)和右边长度为mx个字符是对称相等的。如果 mx>i,i也在这个回文半径内,那么i在id的左边一定有一个对称点,假设这个对称点的坐标为j,则 id - j = i - id,整理一下就是:j = 2*id -i 。由于以id为中心,长度为mx的左右两边对称相等。同理,左边以j为中心的小回文串,一定和右边 以i为中心的小回文串相同,按理说,p[i]=p[j],但这时p[i]的计算还要分两种情况考虑:
2.1.1 第一种情况
第一种情况如下图,以i为中心的小回文半径长度小,它的回文串没有超过mx,也就是 p[i]<mx,蓝色的小回文串包含在红色的大回文串中,这种情况下p[i]=p[j]。
2.1.2 第二种情况
如下图,以id(字符‘h’)为中心的回文串半径是9:abcbaefg h gfeabcba。i的位置是指向右边的字符‘c’,mx是指向最右边的字符‘a’,此时mx>i。i的对称点j是指向左边的字符‘c’。在第二种情况中,虽然以id为中心,半径是9的回文串中左右红框内的字符串是对称相等的,但以j为中心的小回文和以i为中心的小回文串并不一样。以j为中心的小回文串半径是5,以i为中心的小回文串半径是3,p[i]≠p[j],如果i+p[j]就超出了mx的范围,也就是mx-i<p[j]。根据以id为中心,最远到mx的回文串左右对称相等的性质可知,以i为中心的小回文串半径最多到mx位置,也就是这种情况下,p[i]=mx-i。
综合上面两种情况,p[i]要么等于p[j],要么等于mx-i。写成代码就是:
if( mx > i)
p[i]=MIN( p[2*id-i], mx-i)
2.2 当(mx<i)时
当i>mx时,i已经超出了已处理过的回文串的最远位置。这种情况下是无法用i的对称点j的p[j]来推导p[i]的,因为他们不在一个大的回文串范围内,所以他们并不对称相等。
这时,只能先让p[i]=1,然后通过一个while循环,依次判断i两边的每一对字符是否对称相等,如果是则p[i]++,直到不相等。这样就完成了p[i]的计算了。代码如下:
if (mx>i) p[i]=min(p[2*id-i],mx-i);
else p[i]=1;
while (s[i+p[i]]==s[i-p[i]]) p[i]++;
另外,当计算好了当前的p[i]后,还要检查一下是否要更新mx和id的值,也就是说,如果新计算出来的回文半径p[i]大于原来记录的最远距离mx,就用p[i]代替原来的mx,同时用i代替原来的id,代码如下:
if (mx < i + p[i]) {
mx = i + p[i];
id = i;
}
完整代码如下:
Manacher(string s) {
// ----在原串中插入 '#'----------------
string t = "$#";
for (int i = 0; i < s.size(); ++i) {
t += s[i];
t += "#";
}
// ----处理串 t ---------------
vector<int> p(t.size(), 0);
int mx = 0, id = 0;
for (int i = 1; i < t.size(); ++i) {
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
while (t[i + p[i]] == t[i - p[i]]) ++p[i];
if (mx < i + p[i]) {
mx = i + p[i];
id = i;
}
}
}