1. 前言
给定一个长度为 nnn 的字符串 S,Manacher 算法可以在 O(n)O(n)O(n) 的时间内计算出分别以 S 中的每个字符为中心,最大回文子串的长度. 通常 Manacher 算法可以用来求解一个字符串中的最长回文子串,或者是统计一个字符串中所有回文子串的个数等.
2. 字符串预处理
首先需要对带求解的字符串 S 填充分隔符,在每个字符的两边都插入一个特殊的符号,这里我们取 #
作为分隔符,比如 ABCD
填充之后变成了 #A#B#C#D#
;再比如 ABC
填充之后变成了 #A#B#C#
. 这样做的好处是能够让填充后的字符串长度始终为奇数.
3. 算法过程
令数组 P[i] 记录以字符 S[i] 为中心的最长回文子串向左右扩展的长度(包含 S[i]),问题的核心就是求解出所有的 P[i],以下图为例.

可以看出,P[i]-1正好是原字符串中以 S[i] 为中心的最长回文串长度.
Manacher 算法采用递推的方式计算 P[i],也就是说在计算 P[i] 时,P[0],P[1],…,P[i-1] 都已经计算完毕了.
具体计算 P[i] 时,该算法使用两个辅助变量 id
和 mx
,其中 id
为 P[0],P[1],…,P[i-1] 中右边界最大的回文子串的中心下标,mx=id+P[id]
,也就是这个子串的右边界( S[mx] 并不在以 id 为中心的最长回文串中). 令 j=2*id-i
,也就是说 j
是 i
关于中心 id
的对称点,开始分类讨论:
-
若
i<mx
:- 若
P[j]<mx-i
,那么以 S[j] 为中心的回文子串完全被包含在以 S[id] 为中心的回文子串中,由于 i 和 j 对称,以 S[i] 为中心的回文子串也同样会完全被包含在以 S[id] 为中心的回文子串中,以 S[j] 为中心的最长回文子串和以 S[i] 为中心的最长回文子串完全相同,所以必然有 P[i] = P[j],如下图.
- 若
P[j]>=mx-i
,以 S[j] 为中心的回文子串不一定完全被包含于以 S[id] 为中心的回文子串中,但是根据回文串的对称性,下图中两个绿框所包围的部分是相同的,也就是说以 S[i] 为中心的回文子串,其向右至少会扩张到 mx 的位置,也就是说P[i]>=mx-i
,至于 mx 之后的部分是否对称,需要继续暴力匹配.
- 若
-
若
i>=mx
:以 i 为中心,暴力向左右匹配.
const int maxn=1e5+5;
int p[maxn<<1];//数组开两倍大小
string init(string s){
string res="#";
for(char ch:s){
res+=ch;
res+='#';
}
return res;
}
void manacher(string s){
int len=s.length();
int id=0,mx=0;
for(int i=0;i<len;++i){
p[i]=i<mx?min(p[2*id-i],mx-i):1;
while(i-p[i]>=0 && i+p[i]<len && s[i+p[i]]==s[i-p[i]]) ++p[i];
if(i+p[i]>mx){
mx=i+p[i];
id=i;
}
}
}
4. 总结
对于字符串中的每一个位置,只进行一次匹配,Manacher 算法的总体时间复杂度为 O(n)O(n)O(n),n 为字符串的长度,填充分隔符后长度是原来的两倍,所以时间复杂度依然是线性的.