Reference
这篇文章共参考了以下两位大佬的文章以及教材《ACM/ICPC算法基础训练教程》:
Manacher算法可用于计算一个字符串中的最长回文子串的长度,相比于传统的动态规划方法,Manacher算法的时间复杂度可以缩短至。
字符串预处理
在进行Manacher之前,要先对原始字符串进行预处理,具体方法是:在字符串首位以及每个字符之间添加一个原字符串中没有的字符,比如 '#' , '$', '^'等,下面的函数可以对原字符串预处理。
string ManacherString(string s){
string res = "";
for(int i = 0; i < s.length(); i++){
res += '#';
res += s[i];
}
res += '#';
return res;
}
在介绍Manacher算法之前,我们首先需要清楚以下几个概念
回文半径 radius
对于原字符串中的每一个字符,以这个字符为中心的最长回文串长度的一半向上取整就是其回文半径。
在Manacher算法中我们需要一个回文半径数组 radius[] 来储存每一个字符的回文半径。具体来看下面的这个例子:
以下标 7 为回文中心的回文串是:"#b#c#b#",长度为7,那么 radius[7] 就是4
最大回文右边界 right
所谓最大回文右边界就是:「以下标 i 及其之前的字符为回文中心的所有回文串所能达到的最大右边界」。举两个例子就能明白啦~
最大回文右边界的对称中心 C
顾名思义,就是最大回文右边界所对应的回文串的回文中心。还用之前的例子说明最右回文右边界的对称中心。
Manacher算法
大体上,Manacher算法需要对处理过的字符串M中的每一个字符进行遍历,每一次遍历更新R,C,radius[] 参数。我们这里以遍历过程中下标 i 的位置进行分类讨论:
-
第一种情况:下标 i 下一个位置是最大回文右边界的右边,也就是。
在这种情况下,以当前下标 i 为对称中心,不断向左右两侧扩散检查左右两侧的字符是否相等,同时更新回文半径
radius[i] = 1;
while(i + radius[i] < M.length() && i - radius[i] > -1){
if(M[i - radius[i]] == M[i + radius[i]])
radius[i]++;
else break;
}
最后更新最大回文右边界及对称中心。
- 第二种情况:
1.下标 i 的下一个位置是最大回文右边界或者小于最大回文右边界,并且对称中心的回文左边界小于当前下标 i 以为对称中心的对称点的回文左边界。
可能听起来会有点绕口,看了下面的图就明白啦~
在这种情况下,下标 i 处的回文半径就是的回文半径啦~,由于 i 之前的所有下标的回文半径均已算出,所以就不用再使用向两侧扩散的方法去计算回文半径了,。的计算方式也很简单,用对称中心的下标减去当前下标 i 与对称中心的差值就好啦,所以。
2.下标 i 的下一个位置不在最大回文右边界的右边,且。
这种情况由于下标 i 的对称点的长度超出了,所以 i 的回文半径就是 。
3.下标 i 的下一个位置不在最右回文串的右边,且。
这种情况下,我们无法确定下标 i 的回文右边界是否在最右回文右边界之后,所以需要以 i 为中心向两端搜索其最大回文半径,不过因为已经保证区间一定在下标 i 的回文半径范围内,所以只要从开始搜索即可。
Manacher算法实现(C++)
string ManacherString(string s){
string res = "";
for(int i = 0; i < s.length(); i++){
res += '#';
res += s[i];
}
res += '#';
return res;
}
int Manacher(string s){
//radius : 以每一个位置为回文中心求出的回文半径长度
//R : 最大回文右边界,以当前位置i及其之前的字符为回文中心的回文串所能达到的最右边界
//C : 当前指针i之前且与R最近的回文中心
if(s.length() == 0)return 0;
string M = ManacherString(s);
vector<int>radius(M.length());
int R = -1; //最右回文右边界
int C = -1; //最右回文右边界对称中心
int sup = -0x3f3f3f3f;
for(int i = 0; i < radius.size(); i++){
radius[i] = R > i ? min(radius[2 * C - i], R - i + 1) : 1;
while(i + radius[i] < M.length() && i - radius[i] > -1){ //从中心向两边扩寻找半径
if(M[i-radius[i]] == M[i + radius[i]])radius[i]++;
else break;
}
if(i + radius[i] > R){ //更新最大回文右边界及对称中心
R = i + radius[i] - 1;
C = i;
}
sup = max(sup, radius[i]);
}
return sup - 1;
}