Manacher

Manacher

下面的叙述中,约定字符串下标从 0 开始。

定义

Manacher 算法应用于一个特定场景:静态求一个字符串的最长回文子串。复杂度 O(N),是这种场景中效率最高的回文串算法。

首先考虑暴力法:枚举中心点,向左右扩展,判断它左右对称的位置是否相同。暴力法的复杂度上界显然是 O(N2) 的。

考虑暴力法低效的原因,是因为有大量的重复检查。Manacher 就是利用回文串的性质,通过辅助数组 P 来改善,最终做到 O(N) 的复杂度。

实现

首先,因为回文串分长度为奇数和偶数两种情况,所以先对字符串做一个特殊处理:在 S 的每个字符的左右插入一个奇怪字符,比如 #。那么就把长度为奇数的 abcba 变换成了 #a#b#c#b#a#,中心字符为 a;把长度为偶数的 abba 变换成了 #a#b#b#a#,中心字符为 #。相当于把两种情况简化为了只有奇数一种情况。

然后是 Manacher 算法的核心:定义数组 P(i) 表示以字符 Si 为中心字符的最长回文串的半径。显然,如果已经计算出 P(i),那么最大的 P(i)1 就是答案,且这个回文串的开头位置是 iP(i)2。现在的任务是高效地计算 P(i)

假设已经计算出了 P(0)P(i1),下一步继续计算 P(i)。令 RP(0)P(i1) 这些回文串中最大的右端点,C 是这个回文串的中心点。显然 R=C+P(C)。现在 R 左边的字符都已经检查过,只有 R 右边的字符还没有检查。

下面计算 P(i),设 ji 关于 C 的镜像点,P(j) 已经计算出来,然后分类讨论:

  • iR,由于 R 右边的字符还没有检查过,所以只能暴力扩展;
  • i<R,则按照 j 和回文串 C 的左端点的位置关系细分为两种情况:若 jC 的左端点右侧,因为以 i 为结尾的回文串不会越过 R,得 P(i)=P(j)=P(2Ci) 作为初始值,然后暴力扩展完成 P(i) 的计算;若 jC 的左端点左侧,则只能先计算在 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 算法可以求解具有回文串类似的性质的问题,也就是满足 i[1,n]check(Si,Sni+1)=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;
	}
}

注意边界情况的特判。

例题

  • [BZOJ3790] 神奇项链

    求出原串的所有回文子串,记录右端点,问题即转化为用最少的线段覆盖整个区间的贪心问题。贪心地记录最右端点即可。

  • P1659 [国家集训队] 拉拉队排练

    求出 P(i) 后,统计每种长度的回文串的出现次数,如果 k 比回文串的种类多则无解。考虑到如果有长度为 i 的回文串则一定有长度为 i2 的回文串,所以求后缀和就可以知道每种串的出现次数。长度为 i 的回文串给答案的贡献是 icnt(i),其中 cnt(i) 是它的出现次数。

  • P2601 [ZJOI2009] 对称的正方形

    二维回文,略显毒瘤。考虑一个合法正方形的充要条件就是每一行和每一列都是回文串,那么先对每一行和每一列各跑一边 Manacher 并记录下 P(i)。然后考虑到以一个点为中心,如果在横向上能扩展半径为 r 的单位,那么这些单位在纵向上的最长回文半径肯定是不能小于 r 的。于是用二分找到以每个点为中心能横向扩展的最长长度。纵向同理。然后对于每个中心的横向/纵向最长扩展长度取 min 并累加即可。注意我们依然需要在字符间添加 # 字符来统计,但最后能作为中心的 # 字符仅包含满足其左上/左下/右上/右下都是数字的 # 字符。

posted @   Laoshan_PLUS  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示