问题引入:求一个字符串 s s s 里的回文子串(连续)的长度最大是多少?
例如 s = " a a b a b a " s="aababa" s="aababa",最长回文子串的长度就是 5 5 5 ( s 2 s 3 s 4 s 5 s 6 = a b a b a s_2s_3s_4s_5s_6=ababa s2s3s4s5s6=ababa)。
主要思路:
S o l 1 : Sol\ 1: Sol 1: 暴力
这个应该不用多说了,枚举中点,时间复杂度 O ( n 2 ) \mathcal O(n^2) O(n2)。
S o l 2 : Sol\ 2: Sol 2: 二分+哈希
同样是枚举中点,然后二分回文串长度,时间复杂度 O ( n l o g n ) \mathcal O(nlogn) O(nlogn)
S o l 3 : Sol\ 3: Sol 3: 就是我们所讲的manacher(马拉车)算法,时间复杂度 O ( n ) \mathcal O(n) O(n)。
1. l e n len len 数组
回到 S o l 1 Sol\ 1 Sol 1,类比KMP算法,没有考虑到已经计算的部分对于之后结果的贡献,优化朴素方法的突破口就在这里了。
我们知道,回文串分奇回文串和偶回文串两种,奇回文串就是长度为奇数的回文串,如 a a b a a aabaa aabaa;偶回文串就是长度为偶数的回文串,如 y c x x c y ycxxcy ycxxcy。
为了避免分两种情况,我们可以在相邻两个字符之间插入一个 ∣ | ∣ 号,假设 s = " a a b a b a " s="aababa" s="aababa",那么经过此次操作我们的字符串就变成了 ∣ a ∣ a ∣ b ∣ a ∣ b ∣ a |a|a|b|a|b|a ∣a∣a∣b∣a∣b∣a。
为了防止数组越界(由于在最前面的 ∣ | ∣的前一个字符和最后面的 ∣ | ∣的后一个字符都是’\0’,可能会导致 w h i l e while while陷入死循环),我们可以在最前面补上一个’#'符号。这样一来,我们字符串就变为
i = i= i= 0 1 2 3 4 5 6 7 8 9 10 11 12 0\ \ \ 1\ \ \ 2\ \ \ 3\ \ 4\ \ 5\ \ \ 6\ \ 7\ \ \ 8\ \ 9\ \ 10\ \ 11\ 12 0 1 2 3 4 5 6 7 8 9 10 11 12
s = s= s= # ∣ a ∣ a ∣ b ∣ a ∣ b ∣ a \#\ \ \ |\ \ \ a\ \ \ |\ \ \ a\ \ \ |\ \ \ b\ \ \ |\ \ \ a\ \ \ |\ \ \ b\ \ \ |\ \ \ a # ∣ a ∣ a ∣ b ∣ a ∣ b ∣ a。
我们定义 l e n i len_i leni为以 i i i为中心的回文串的最大半径,如在上面的例子中, l e n 8 = 6 len_8=6 len8=6 (由于 ∣ a ∣ b ∣ a ∣ b ∣ a ∣ |a|b|a|b|a| ∣a∣b∣a∣b∣a∣为回文串,半径为 6 6 6), l e n 3 = 3 len_3=3 len3=3 (由于 ∣ a ∣ a ∣ |a|a| ∣a∣a∣为回文串,半径为 3 3 3)我们不难发现,对于每一个 i i i , l e n i − 1 len_i-1 leni−1就是对应的原串中的回文子串的长度。
2.如何求 l e n len len 数组?
假设我们现在再求 l e n i len_i leni,那么所有 j ( 0 ≤ j < i ) j\ (0 \leq j \lt i) j (0≤j<i),它们的 l e n len len值是已经求得的,那么对于每一个 k k k 都会有一个相应的回文串区间[ k − l e n k , k + l e n k k-len_k,k+len_k k−lenk,k+lenk]。假设所有区间右端点的最大值为 m x mx mx ,取得最大值的 k k k 为 i d id id。
在下图中,划下划线的部分就是对应的已知的回文串
对于 i < m x i \lt mx i<mx 的情况,假设 i i i 关于 i d id id 的对应点为 j j j, m x mx mx 关于 i d id id 的对称点为 m x ′ mx' mx′,那么对于所有的 0 ≤ l ≤ l e n j 0 \leq l \leq len_j 0≤l≤lenj, s j − l = s j + l s_{j-l}=s_{j+l} sj−l=sj+l,又对于 0 ≤ l ′ ≤ l e n i d 0 \leq l' \leq len_id 0≤l′≤lenid, s i d − l ′ = s i d + l ′ s_{id-l'}=s_{id+l'} sid−l′=sid+l′,因此对于$ 0 \leq l \leq min(len_j,j-mx’) , , ,len_{i+l}=len_{j-l}=len_{j+l}=len_{i-l}$,可以将 l e n i len_i leni 赋上初值 m i n ( l e n j , j − m x ′ ) min(len_j,j-mx') min(lenj,j−mx′),即 m i n ( l e n j , m x − i ) min(len_j,mx-i) min(lenj,mx−i)。
对于 i ≥ m x i \geq mx i≥mx 的情况,我们不知道之前的情况,只好将 l e n i len_i leni 赋上初值 1 1 1。
两种情况赋上初值之后,分别向外扩展直到不能再扩展即可。
3.时间复杂度
和Z算法类似,只有遇到还没有匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,所以对于字符串中的每一个位置,只进行一次匹配。由于字符串的长度是线性 O ( 2 ∣ s ∣ ) \mathcal O(2|s|) O(2∣s∣) 的,因此时间复杂度也是 O ( n ) \mathcal O(n) O(n) 的。
4.代码:
以 P3805 模板题为例,代码如下。
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define put(x) putchar(x)
#define eoln put('\n')
#define space put(' ')
inline int read(){
int x=0,neg=1;char c=getchar();
while(!isdigit(c)){
if(c=='-') neg=-1;
c=getchar();
}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*neg;
}
inline void print(int x){
if(x<0){
putchar('-');
print(abs(x));
return;
}
if(x<=9) putchar(x+'0');
else{
print(x/10);
putchar(x%10+'0');
}
}
char s[22000005];//由于要添上|分隔符,空间要开2倍
int len[22000005],n=1;
inline void in(){//读入,由于题目输入量很大,要进行读入优化
char c=getchar();
s[0]='#',s[1]='|';//字符串操作添加#(防越界)和|(防分类讨论)两种预处理
while(c<'a'||c>'z') c=getchar();
while(c>='a'&&c<='z') s[++n]=c,s[++n]='|',c=getchar();
}
void Manacher(){
int pos=0,mx=0;
for(int i=1;i<=n;++i) {
len[i]=i<mx?min(len[pos*2-i],mx-i):1;//关键语句,赋初值,分i<mx和i>=mx两种情况
while(s[i-len[i]]==s[i+len[i]]) len[i]++;//向外扩展
if(i+len[i]>mx) mx=i+len[i],pos=i;//更新mx和pos
}
}
int main(){
in();
Manacher();
int ans=0;
fz(i,1,n) ans=max(ans,len[i]);
cout<<ans-1<<endl;//对于每一个$i$,以它为中心的最长回文子串的长度为len[i]-1
return 0;
}
注:本博客中的图片为https://www.cnblogs.com/Syameimaru/p/9310883.html中的图片