Manacher算法
算法简介
(Manacher) 算法,即“马拉车”算法,是一种高效((O(n)))的求最长回文子串的算法。相比于 (KMP) ,(Manacher) 也许更好理解一些。
算法原理
对于传统的暴力解法,求最长回文子串的方式应该是对于每个位置 (i) ,向两边“扩大”以寻找回文子串,再记录答案。易证,此解法时间复杂度为 (O(n^2))。
众所周知,每一个毒瘤算法都有一个暴力的开头。
(Manacher) 算法首先要对原字符串进行处理。因为回文串分为两种,“奇回文”和“偶回文”:
"abcba"
"abba"
首先,在每两个字符之间放入一个没有出现在原串的字符,如 (mathtt{'#'}) ;接着,将字符串的两边插入另外两个不同的,且没有出现在原串的字符,如 (mathtt{'@'}) 和 (mathtt{'$'}) 。操作结果如下:
"@a#b#b#a$"
"@a#b#c#b#a$"
神奇的事情发生了,“奇回文”和“偶回文”都转化成了“奇回文”。同时,在判定回文串的过程中也不用判边界了,因为修改后字符串边界字符不一样。
除此之外, (Manacher) 算法还引入了一个数组 (p) , (p_i) 表示以位置 (i) 为中心的“最长回文半径”。对于字符串 (mathtt{"@#a#b#b#a#d#c#a#c#d#a#$"}) 如表所示:
(i) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(s_i) | @ | # | a | # | b | # | b | # | a | # | d | # | c | # | a | # | c | # | d | # | a | # | $ |
(p_i) | 1 | 1 | 2 | 1 | 2 | 3 | 2 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 1 |
显然,最终答案就是 (max{p_i-1}) 。那么 (Manacher) 算法就讲完了……等等, (p) 数组怎么求?
算法过程
首先,看下图:
引入两个变量, (mx) 和 (id) , (mx) 表示以 (id) 为中心点的最大回文子串右边界。
假设现在要求 (p_i) ,那么就有:
if(i<mx)
p[i]=min(p[id*2-i],mx-i);
这样就可以加快速度。但是……为什么?为什么为什么???
设 (i'=id*2-i) ,也就是说,(i') 是 (i) 关于 (id) 的对称点。因为是从前往后循环, (p_{i'})是已知的。下面开始分类讨论:
- 若 (p_{i'}>mx-i) ,可得以 (i') 为中心,以 (mx) 的对称点 (mx') 到 (i') 的距离为半径形成的回文字符串是存在的,并且 (id) 到 (mx') 与 (id) 到 (mx) 一一对应,所以 (mx) 是 (i) 目前可以更新到的最大回文半径;
- 若 (p_{i'}<mx-i) ,也就是 (i') 的回文半径小于 (mx') 到 (i') 的距离,可得 (p_i=p_{i'}) 。
于是就有了你看到的反人类代码。
完整代码
int manacher(string &s_src)
{
/*
* @params s_src为源字符串,s为预处理后的字符串
*/
string s="@";
for(auto &ch: s_src)
{
s.push_back('#');
s.push_back(ch);
}
s+="#$";
int id,mx=0,res=0;
for(int i=1;i<s.size()-1;i++)
{
if(i<=mx)
p[i]=min(p[id*2-i],mx-i);
while(s[i-p[i]]==s[i+p[i]])
p[i]++;
if(i+p[i]>mx)
id=i,mx=i+p[i]-1;
res=max(res,p[i]-1);
}
return res;
}