Manacher Algorithm 马拉车算法
在介绍算法前,先了解一下最长回文子串
在计算机科学中,最长回文子串或最长对称因子问题是在一个字符串中查找一个最长连续子串,这个子串必须是回文。例如:
(1) s = “banana” 最长回文子串是“anana”。中间的a为中心,左右对称(或者说镜像)。
(2) s = “abracadabra” 最长回文子串是“ada”和“aca”,长度均为3。
所以最长回文子串并不能保证是唯一的。在一些应用中需要返回全部的最长回文子串(所有子串都是回文,并且不能扩展为更大的回文子串)而不是返回其中之一或是最大的回文子串的长度。
[Manacher(1975)] 发现了一种线性时间算法
O(n)
O
(
n
)
,可以在列出给定字符串中从字符串头部开始的所有回文。要在线性时间内找出字符串的最长回文子串,这个算法必须利用回文和子回文的这些特点和观察回文的左边是右边的镜像。
算法流程分析
由于回文分为偶回文(比如 abab 长度为4)和奇回文(比如 abcba 长度为5),而在处理奇偶问题比较麻烦,所以这里需要做个预处理,在字符间插入一个特殊字符(这个字符不能在串里出现
),将原串转换统一成奇串。
比如原字符串: s =”abbaTNTabcba”
插入字符之后:sNew= “$
#a#b#b#a#T#N#T#a#b#c#b#a#”(开头的$是为了防止越界,在下面的代码注释中有体现)
原串s中含有一个偶回文abba和两个奇回文baTNTab、abcba,插入'#'
字符后长度都转换成了奇数,比如:#a#b#b#a#长度为9、#b#a#T#N#T#a#b#长度为15。
算法需要一个与新串sNew等长的辅助数组vector<int> p(sNew.size(),0)
,其中p[i]表示以sNew[i]为中心的最长回文子串的半径,若p[i]=1,则该回文子串就是sNew[i]本身。下面我们将新串sNew的最大回文子串半径列出:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
s[i] | $ | # | a | # | b | # | b | # | a | # | T | # | N | # | T | # | a | # | b | # | c | # | b | # | a | # | \0 |
p[i] | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 | 2 | 1 | 8 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 |
由于表格太长,将第二行头sNew[i]省写为s[i],最右边s[26]='\0' 可能会被挡住了
例如:以sNew[20]=’c’为中心的最长回文子串半径为6。
由于第一个和最后一个字符都是#号,且也需要搜索回文,为了防止越界,由于字符串在结尾有’\0’,所以在字符串开头需要加上非#号字符(为了区分这里用的$)。通过p数组可以找到最大回文子串半径的最大值及其中心位置,就能确定最长回文子串了。接下来的问题是如何求p数组?
Manacher算法利用开头提到的回文的左边是右边的镜像,让回文串起始的对比位置尽可能的大。
图1
图2
图注:id为已知的最大回文子串中心的位置,mx是已知最大回文串的右边界,i为当前遍历到字符串的位置。
这里有两种情况讨论:
一、mx > i
假设当前遍历到字符串的位置i
,由于在遍历到id
位置的时候已知最大回文子串,位置i
还在上一个最大回文子串的范围内,所以可以利用其镜像认为,位置i
以id
为中心镜像到另一边的位置j
是对等的。 在mx>i的条件下,又分为以下两种情况:
1. mx - i > p[j]
(图1)
此时,以j为中心的回文子串包含在以id为中心的回文子串内,由于i和j位置对等,所以以i为中心的回文子串包含在以id为中心的回文子串内,所以p[i] = p[j] = p[2 * id - i]
。
2. mx - i <= p[j]
(图2)
此时,以j为中心的回文子串超过了以id为中心的回文子串边界,但是由于i和j位置对等,绿框部分还是相同的。所以其向右延伸的范围最大就是mx-i
,剩下超过的部分谁也不能保证是否一致,只能通过循环对比判断,所以p[i] = mx - i
。
二、mx < i
此时镜像对预判位置起不到作用,只能从长度为1开始对比,所以p[i] = 1
。
C++实现
#include<string>
#include <vector>
#include<iostream>
#include <algorithm>
using namespace std;
string Manacher(string s)
{
string sNew = "$#";
for (auto iter = s.cbegin(); iter != s.cend(); iter++)
{
sNew += *iter;
sNew += "#";
}
int iNewSize = sNew.size();
int iMaxSubStringLength = -1; // 最长回文子串的长度
int iMaxSubStringPos = -1; // 最长回文子串中心点的位置
vector<int> p(iNewSize, 0);
int id = 0;
int mx = 0;
for (int i = 1; i < iNewSize; i++)
{
if (i < mx)
{
p[i] = min(p[2 * id - i], mx - i);
}
else
{
p[i] = 1;
}
while (sNew[i - p[i]] == sNew[i + p[i]]) // 最左边sNew[0]='$',最右边sNew[sNew.size()] = '\0',无需判断边界
{
p[i]++;
}
if (mx < i + p[i]) //我们每走一步i,都要和mx比较,我们希望mx尽可能的远,这样才能更有机会执行if (i < mx)这句代码,从而提高效率
{
id = i;
mx = i + p[i];
}
if (p[i] - 1 > iMaxSubStringLength)
{
iMaxSubStringLength = p[i] - 1;
iMaxSubStringPos = i;
}
}
auto iStart = s.cbegin() + (iMaxSubStringPos - iMaxSubStringLength - 1) / 2; // 将最长回文子串起始位置转换回原串
return string(iStart, iStart + iMaxSubStringLength);
}
void main()
{
string s = "abbaTNTabcba";
cout << s <<" 的最长回文子串为: " << Manacher(s) << endl;
getchar();
}
输出: abbaTNTabcba 的最长回文子串为: baTNTab