马拉车算法是用来查找一个字符串的最长回文串的线性方法。正读与反读都一样的字符串称为回文字符串。比如 “google” 的最长回文子串为 “goog”。这个算法的总框架是,遍历所有的中心点,寻找每个中心点对应的最长回文子串,然后找到所有中心点对应的最长回文子串。
由于回文串的长度可以是奇数也可以是偶数。比如:non,与noon。故马拉车算法的第一步是对字符串做预处理:在每一个字符的左右都加上一个特殊的字符,如 # 则 non ——>#n#o#n# noon——>#n#o#o#n# 经过了预处理,不论原来的字符串是奇数个还是偶数个,现在都是奇数个。
接下来需要一个 与处理后的字符串t等长 的数组p , p[i]记录 以t[i]为中心的回文串的半径(不包括p[i]本身)
若p[i]=0表示,该回文子串就是t[i]自身。比如 “#a#b#” 的半径数组为 [0, 1, 0, 1 0]。
为了避免在搜索回文子串时,总是判断数组是否越界,我们可以在字符串 t 的首尾各加上一个标志字符。(注意这两个字符应在字符串 t 中没有出现过)。如:#n#o#n# ——> ^#n#o#n# $ 数组 p 的最大半径,就是我们要寻找的最长回文子串的半径,只要把 p 数组求出来,我们便可以找到答案了。
如何计算p 数组:
马拉车算法在计算 p 数组时,一直在更新两个变量:
id : 回文子串的中心位置 mx: 回文子串的末端位置
使用这两个便可以用一次扫描来计算出整个p数组。
公式: p[i] = min( p[2*id - i] , mx - i);
当 mx - i > p[j]时,以 s[j] 为中心的回文子串 必定包含在 以s[id]为中心的回文子串中, 由于 i 和 j 对称 ,以 s[i]为中心的回文子串 也一定包含在 以s[id]为中心的回文子串中,所以p[i]=p[j], 由于 j 到id之间的距离 等于 id到 i 的距离。 既 id - j = i - id 移项可得 j = 2*id - i
当 mx - i <=p[j]时, 以 s[j] 为中心的回文子串不一定完全包含于以 s[id]为中心的回文子串中,据对称性可知,下图中两个绿框所包围的部分相同,意思就是,以s[i]为中心的回文子串,向右可以扩张到mx位置,即 p[i] = mx - i; 对于mx之后的部分是否对称,可以用while循环去判断。
代码:
void init()
{
int k=0;
str[k++] = '$';//字符串起始位置的特殊标志//字符串末不需要再加标志了 '\n'就可以代替了 //防溢出
for(int i=0;i<len;i++)
{
str[k++]='#';
str[k++]=s[i];
}
str[k++]='#';//最后一个字母后的符号
len=k;//字符串更新后的长度为k
}
int Manacher()
{
p[0] = 0;
int sum = 0;//记录最长回文串
mx = 0;
for(int i=1;i<len;i++)
{
if(i < mx) p[i] = min(mx - i, p[2 * id - i]);
else p[i] = 1;
/*p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;*/
while(str[i - p[i]]== str[i + p[i]]) //向两边扩展更新p[i] //因为预处理的设置,这里不必担心溢出问题
p[i]++;
if(p[i] + i > mx)//回文半径变大
{
mx = p[i] + i;//更新回文串能达到的末端位置
id = i;//更新回文串中心点
sum = max(sum, p[i]);//更新回文串的最长长度
}
}
return (sum - 1);
}