Manacher算法讲解

问题引入:求一个字符串 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 aababa

为了防止数组越界(由于在最前面的 ∣ | 的前一个字符和最后面的 ∣ | 的后一个字符都是’\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| ababa为回文串,半径为 6 6 6), l e n 3 = 3 len_3=3 len3=3 (由于 ∣ a ∣ a ∣ |a|a| aa为回文串,半径为 3 3 3)我们不难发现,对于每一个 i i i l e n i − 1 len_i-1 leni1就是对应的原串中的回文子串的长度。

2.如何求 l e n len len 数组?

假设我们现在再求 l e n i len_i leni,那么所有 j   ( 0 ≤ j < i ) j\ (0 \leq j \lt i) j (0j<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 klenk,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 0llenj s j − l = s j + l s_{j-l}=s_{j+l} sjl=sj+l,又对于 0 ≤ l ′ ≤ l e n i d 0 \leq l' \leq len_id 0llenid s i d − l ′ = s i d + l ′ s_{id-l'}=s_{id+l'} sidl=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,jmx),即 m i n ( l e n j , m x − i ) min(len_j,mx-i) min(lenj,mxi)

对于 i ≥ m x i \geq mx imx 的情况,我们不知道之前的情况,只好将 l e n i len_i leni 赋上初值 1 1 1

两种情况赋上初值之后,分别向外扩展直到不能再扩展即可。

3.时间复杂度

Z算法类似,只有遇到还没有匹配的位置时才进行匹配,已经匹配过的位置不再进行匹配,所以对于字符串中的每一个位置,只进行一次匹配。由于字符串的长度是线性 O ( 2 ∣ s ∣ ) \mathcal O(2|s|) O(2s) 的,因此时间复杂度也是 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中的图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值