Manacher
Manacher
下面的叙述中,约定字符串下标从 \(0\) 开始。
定义
Manacher 算法应用于一个特定场景:静态求一个字符串的最长回文子串。复杂度 \(O(N)\),是这种场景中效率最高的回文串算法。
首先考虑暴力法:枚举中心点,向左右扩展,判断它左右对称的位置是否相同。暴力法的复杂度上界显然是 \(O(N^2)\) 的。
考虑暴力法低效的原因,是因为有大量的重复检查。Manacher 就是利用回文串的性质,通过辅助数组 \(P\) 来改善,最终做到 \(O(N)\) 的复杂度。
实现
首先,因为回文串分长度为奇数和偶数两种情况,所以先对字符串做一个特殊处理:在 \(S\) 的每个字符的左右插入一个奇怪字符,比如 \(\tt\#\)。那么就把长度为奇数的 \(\tt abcba\) 变换成了 \(\tt\#a\#b\#c\#b\#a\#\),中心字符为 \(\tt a\);把长度为偶数的 \(\tt abba\) 变换成了 \(\tt\#a\#b\#b\#a\#\),中心字符为 \(\tt\#\)。相当于把两种情况简化为了只有奇数一种情况。
然后是 Manacher 算法的核心:定义数组 \(P(i)\) 表示以字符 \(S_i\) 为中心字符的最长回文串的半径。显然,如果已经计算出 \(P(i)\),那么最大的 \(P(i)-1\) 就是答案,且这个回文串的开头位置是 \(\dfrac{i-P(i)}2\)。现在的任务是高效地计算 \(P(i)\)。
假设已经计算出了 \(P(0)\sim P(i-1)\),下一步继续计算 \(P(i)\)。令 \(R\) 为 \(P(0)\sim P(i-1)\) 这些回文串中最大的右端点,\(C\) 是这个回文串的中心点。显然 \(R=C+P(C)\)。现在 \(R\) 左边的字符都已经检查过,只有 \(R\) 右边的字符还没有检查。
下面计算 \(P(i)\),设 \(j\) 是 \(i\) 关于 \(C\) 的镜像点,\(P(j)\) 已经计算出来,然后分类讨论:
- 若 \(i\ge R\),由于 \(R\) 右边的字符还没有检查过,所以只能暴力扩展;
- 若 \(i<R\),则按照 \(j\) 和回文串 \(C\) 的左端点的位置关系细分为两种情况:若 \(j\) 在 \(C\) 的左端点右侧,因为以 \(i\) 为结尾的回文串不会越过 \(R\),得 \(P(i)=P(j)=P(2C-i)\) 作为初始值,然后暴力扩展完成 \(P(i)\) 的计算;若 \(j\) 在 \(C\) 的左端点左侧,则只能先计算在 \(R\) 内的 \(P(i)\) 部分作为初始值,然后再用暴力中心扩展法。
实际代码实现中,这两种情况可以合并计算。
代码(P3805 【模板】manacher)
比较好的一点是,遇到多测,\(P\) 数组无需清空。
#include<bits/stdc++.h>
using namespace std;
constexpr int MAXN=1.1e7+5;
int n,P[MAXN<<1];
string s1,s;
void init(){
s+="$#";
for(auto x:s1) s+=x,s+='#';
s+='&';
n=s.size();
}
// 背板!
void manacher(){
int R=0,C=0;
for(int i=1;i<n;i++){
P[i]=i<R?min(P[(C<<1)-i],P[C]+C-i):1;
while(s[i+P[i]]==s[i-P[i]]) P[i]++;
if(R<P[i]+i) R=P[i]+i,C=i;
}
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin>>s1;
init();
manacher();
cout<<*max_element(P,P+n)-1<<'\n';
return 0;
}
复杂度
实际上,Manacher 算法的本质是在扩展 \(R\)。而因为 \(R\) 的扩展是单调的,所以每个字符最多被扩展到一次,因此复杂度是 \(O(2N)=O(N)\) 的。
后记
实际上,Manacher 算法可以求解具有回文串类似的性质的问题,也就是满足 \(\forall i\in[1,n]\),\(\operatorname{check}(S_i,S_{n-i+1})=\texttt{true}\) 的问题,都可以用 Manacher 求解。普遍来说代码是这样的:
void manacher(){
int R=0,C=0;
for(int i=1;i<n;i++){
P[i]=i<R?min(P[(C<<1)-i],P[C]+C-i):check(i,i);
while(check(i-P[i],i+P[i])) P[i]++;
if(R<P[i]+i) R=P[i]+i,C=i;
}
}
注意边界情况的特判。
例题
-
求出原串的所有回文子串,记录右端点,问题即转化为用最少的线段覆盖整个区间的贪心问题。贪心地记录最右端点即可。
-
求出 \(P(i)\) 后,统计每种长度的回文串的出现次数,如果 \(k\) 比回文串的种类多则无解。考虑到如果有长度为 \(i\) 的回文串则一定有长度为 \(i-2\) 的回文串,所以求后缀和就可以知道每种串的出现次数。长度为 \(i\) 的回文串给答案的贡献是 \(i^{\text{cnt}(i)}\),其中 \(\text{cnt}(i)\) 是它的出现次数。
-
二维回文,略显毒瘤。考虑一个合法正方形的充要条件就是每一行和每一列都是回文串,那么先对每一行和每一列各跑一边 Manacher 并记录下 \(P(i)\)。然后考虑到以一个点为中心,如果在横向上能扩展半径为 \(r\) 的单位,那么这些单位在纵向上的最长回文半径肯定是不能小于 \(r\) 的。于是用二分找到以每个点为中心能横向扩展的最长长度。纵向同理。然后对于每个中心的横向/纵向最长扩展长度取 \(\min\) 并累加即可。注意我们依然需要在字符间添加 \(\tt\#\) 字符来统计,但最后能作为中心的 \(\tt\#\) 字符仅包含满足其左上/左下/右上/右下都是数字的 \(\tt\#\) 字符。