Manacher算法
Manacher 算法可在 \(O(n)\) 解决最长回文串的问题。
通过预处理 \(d_i\) 表示以 \(i\) 为中心的回文串向两边延伸的最长长度来解决原问题。
如对于字符串 abcba
(下标从\(1\)开始),\(d_3=3\),即 \(cba\)。
算法流程如下:
改造字符串
在开头插入 $
,末尾插入 !
,每个字符的左边和右边都插入一个 #
。
例如:
abcdefg
改造为:
$#a#b#c#d#e#f#g#!
在开头和末尾插入两个不同的字符,可以避免边界问题。而在相邻字符插入 #
,则是为了处理长度为偶数的回文串,例如:aa
转化为 #a#a#
,即变为以第二个 #
为中心的回文串。
预处理 \(d_i\)
在预处理 \(d_i\) 时,需要维护一个表示当前右端点最右边的回文串区间,不妨用 \([l,r]\) 表示。
假设现在正在处理 \(i\),那么分为两种情况:
\(i\in [l,r]\)
对于 \(i\) 在 \([l,r]\) 中,可以找到一些性质,记 \(i'\) 表示 \(i\) 关于 \(\frac{l+r}{2}\) 的对称点(注意:此时 \(d_{i'}\) 已经求出),那么分为以下几种情况:
\(i'\) 的回文长度未超出 \([l,r]\)(下图蓝色为回文部分)
由于 \([l,r]\) 本身为回文串,所以在对称位置,回文情况是一致的,那么直接 \(d_i=d_{i'}\) 即可。
\(i'\) 的回文长度超出 \([l,r]\)
那么我们只能确认图中(即在\([l,r]\)内的部分)蓝色部分为回文,至于红色部分(超出区间部分),我们无法确认,只能通过暴力枚举处理。
\(i>r\)
直接暴力往两边扩展即可。
计算完 \(d_i\) 记得更新区间。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=3e7;
int n,d[N];
string s;
char ch[N];
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin>>s;
ch[0]='$';//改造字符串
for(int i=0;i<s.size();i++) {
ch[++n]='#';
ch[++n]=s[i];
}
ch[++n]='#';
ch[++n]='!';
d[1]=1;
for(int l=1,r=1,i=2;i<n;i++) {
if(i>=l&&i<=r) d[i]=min(d[l+r-i],r-i+1);//l+r-i即为i',r-i+1即i到右边界距离
while(ch[i+d[i]]==ch[i-d[i]]) d[i]++;//暴力扩展
if(i+d[i]-1>=r) {r=i+d[i]-1; l=i-d[i]+1;}//更新区间
}
int ans=0;
for(int i=1;i<=n;i++) {
ans=max(ans,d[i]-1);
}
cout<<ans;
return 0;
}
扩展应用
裸的Manacher算法只能求出以一个字符为中心向两边延伸的最长回文长度,但是没法求出以某个点为起点的最长回文长度,需要通过递推的方式求解。
首先预处理出上文的 \(d\) 数组,然后用 \(i\) 和 \(d_i\) 计算出 \([l_i,r_i]\),此时计算出的所有区间,都是尽可能长的区间,也就是说:如果 \([1,5]\)是回文,那么\([2,4]\)也一定是回文,而目前的做法只能处理出 \([1,5]\),处理不出 \([2,4]\)。
所以考虑用 \(dp\) 解决问题,设 \(f_{i}\) 表示以 \(i\) 为开头的最长长度,先用 \(i,d_i\) 计算出一部分 \(f_{i}\)。
从上文 \([1,5]\) 和 \([2.4]\) 的例子也能得到一些启发,即前一个开头的最长回文损失两个(一头一尾)就变成当前位置回文长度,即:
当然,在 Manacher 算法中,由于字符之间用 \(#\) 隔开,所以实际改写为:
类似例题:P4555 [国家集训队] 最长双回文串
提交记录:记录详情