Manacher 学习笔记

引入

求解最长回文子串问题:给出一个字符串 s s s ,求出 s s s​ 最长的回文子串长度
我们的暴力做法就是依次以每个字符或者两个字符之间的空隙为中心,当两侧对应字符相同时向两侧进行扩展,扩展到极限展时,我们就得到了该中心对应的回文子串最大长度,与 a n s ans ans max ⁡ \max max 即可
核心代码如下(默认字符串下标从 1 1 1 开始):

#define rep(i,n) for(int i = 1;i <= n;i++)
inline int solve(char * s){
	int n = strlen(s + 1),ans = 0;
	rep(i,n){
		int l = i,r = i;
		while(l > 1 &&r < n && s[l-1] == s[r+1]) ++l,--r;
		ans = max(ans,r - l + 1);
		if(i < n && s[i] == s[i+1]){
			l = i,r = i + 1;
			while(l > 1 &&r < n && s[l-1] == s[r+1]) ++l,--r;
			ans = max(ans,r - l + 1);
		}
	}
	return ans;
}

但是这个暴力做法的复杂度显然是 O ( n 2 ) O(n^2) O(n2) 的,太慢了,有没有更优一点的做法呢?

Manacher

首先,我们发现我们的暴力程序在判断回文子串的时候需要分长度奇偶,这个分讨有点麻烦,我们不妨在原串的每两个字符之间(包括字符首尾)加入分隔符(通常是不在字符串中出现的 # \# #)构造出新串 t t t 。这样一来,我们就只用讨论长度为奇数的回文子串求解方法了。
我们使用一个数组 p p p 来记录以每个字符为中心的最长回文半径(即用 p i p_i pi 记录以 t i t_i ti 为中心的最长回文子串的半径, p i p_i pi 最小为 1 1 1 )。接下来证明 p i − 1 p_i - 1 pi1 就是以 t i t_i ti 为中心的最长回文子串在原串中的长度

  1. 显然 l e n = p i × 2 − 1 len = p_i \times 2 - 1 len=pi×21 t t t 中以 t i t_i ti 为中心的最长回文子串长度。
  2. t i t_i ti 为中心的最长回文子串一定是以 # \# # 开头和结尾的。所以 l e n len len 减去最前或者最后的 # \# # 字符之后,就是原串中对应回文子串长度的两倍。即原串长度为 ( l e n − 1 ) / 2 = p i − 1 (len - 1) / 2 = p_i - 1 (len1)/2=pi1 ,得证。

所以接下来就是如何求 p p p 数组了。假设我们已经求出了 p 1 , p 2 . . . p i − 1 p_1,p_2...p_{i-1} p1,p2...pi1 ,现在要求 p i p_i pi
我们用一个变量 m a x i d maxid maxid 记录在求解 p i p_i pi 之前,我们找到的回文串中,延伸到最右端的回文串的右端点位置,再用一个变量 i d id id 记录这个回文串的中心的下标。这两个变量的初值都是零。
接下来有如下代码,实现原理会稍后解释

#define rep(i,n) for(int i = 1;i <= n;i++)
void manacher(){
	……
	rep(i,m){
		p[i] = 1;
		if(maxid > i) p[i] = min(p[(id<<1)-i],maxid - i);
		while(t[i-p[i]] == t[i+p[i]]) p[i]++;
		if(i + p[i] > maxid) id = i,maxid = i + p[i];
		……
	}
	……
}

j = i d × 2 j = id \times 2 j=id×2 i i i 关于 i d id id 的对称点,根据回文串的对称性, p j p_j pj 代表的回文串可以对称到 i i i 这边,但是如果 p j p_j pj 代表的回文串对称过来以后超过 m a x i d maxid maxid 的话,超出的部分就不能对称过来了,所以 p i p_i pi 的下限就取 p i d × 2 − i p_{id \times 2 - i} pid×2i m a x i d − i maxid - i maxidi 的较小者。随后我们再暴力扩展 p i p_i pi ,并更新 i d id id m a x i d maxid maxid

以下是我的 Manacher 模板 代码

#include <bits/stdc++.h>
using namespace std;
#define rep(i,n) for(int i = 1;i <= n;i++)
#define itn int
#define ll long long
const int N = 2.3e7;
int p[N],n,m,id,maxid,ans;
char s[N],t[N];
void manacher(){
	n = strlen(s + 1),m = n << 1 | 1;
	t[0] = '*',t[m+1] = '^',t[1] = '#';//注意边界情况
	rep(i,n) t[i<<1] = s[i],t[i<<1|1] = '#';
	rep(i,m){
		p[i] = 1;
		if(maxid > i) p[i] = min(p[(id<<1)-i],maxid - i);
		while(t[i-p[i]] == t[i+p[i]]) p[i]++;
		if(i + p[i] > maxid) id = i,maxid = i + p[i];
		ans = max(ans,p[i]);
	}
	printf("%d",ans - 1);
}
int main(){
	scanf("%s",s + 1);
	manacher();
	return 0;
}

复杂度

我们的字符串 t t t 只被遍历了一次,复杂度为 O ( n ) O(n) O(n) (因为 m = n × 2 + 1 m = n \times 2 + 1 m=n×2+1
现在考虑我们暴力扩展 p i p_i pi 的次数
需要暴力扩展 p i p_i pi ,就意味着在暴力扩展前以 t i t_i ti 为回文中心的回文串右端点就已经到达了 m a x i d maxid maxid 。注意到我们每暴力扩展一次就会使得 m a x i d maxid maxid 增加 1 1 1 ,而 m a x i d maxid maxid 最多为 m m m ,因此在整个过程中最多只会暴力扩展 O ( n ) O(n) O(n) O ( m ) O(m) O(m) ) 次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值