浅谈 Manacher

从某种方面来说,Manacher 算法是朴素 \(O(n^2)\) 暴力算法的优化。。。

那就得先了解一下 Manacher 的朴素算法------

朴素算法

枚举中心点并不断向外展开(例如:\([i,i]\rightarrow [i+1,i+1]\rightarrow [i+2,i+2]\rightarrow \dots\)

缺点:

  1. 时间复杂度:\(O(n^2)\)———慢
  2. 不是特别好处理长度为偶数的回文串———菜

Manacher

想要优化,首先得解决几个问题:

如何处理长度为偶数的回文串?

可以这样:在每个字符串间及开头结尾加上一个特殊字符(例子:aaaa $\rightarrow $ ~#a#a#a#a#

考虑例子中为什么开头第一个地方有个 ~

这个后面代码中再说(防止数组越界)。。。

如何使时间复杂度降到线性?

记录一个数组p[i]表示以 \(i\) 为回文中心的回文半径。

现在就是处理来到 \(i\) 这个点,如何转移p[i]

我们在这里维护一个当前回文串最右端点 \(r\),和其对称中心 \(mid\)

\(i\le r\)

因为 \(i+p[i]-1\le r\)

所以 \(p[i]\le r-i+1\)

我们通过以 \(mid\) 为对称轴得到一个与当前的 \(i\) 对称的点 \(j\)(这个对称点点的p[]已经处理出来)来转移p[i],同时还能向外扩展:while(s[i-p[i]]==s[i+p[i]])++p[i];

\(j\) 就用初一的中点公式:\(\frac{i+j}{2}=mid\rightarrow j=2*mid-i\)

所以 \(p[i]=min(p[2*mid-i],r-i+1)\)

\(i>r\)

\(p[i]=1\),就是不能向外扩展(只有自己本身的长度)。

板子代码:

#include <bits/stdc++.h>
using namespace std;
const int N=3e7+5;
int n,p[N];
char s[N],st[N];
int cnt=1; 
void init()
{
	s[0]='~';
	s[cnt++]='#';
	for(int i=0;i<n;i++)
	{
		s[cnt++]=st[i];
		s[cnt++]='#';
	}
}
int main()
{
	cin>>st;
	n=strlen(st);
	init();
	int ans=-1;
	for(int i=1,r=0,mid=0;i<=cnt;i++)
	{
		if(i<=r)p[i]=min(p[2*mid-i],r-i+1);
		while(s[i-p[i]]==s[i+p[i]])++p[i];//解释一下:s的第一位那个~,就是在while循环中防止越界
		if(i+p[i]>r)r=p[i]+i-1,mid=i;
		if(ans<p[i])ans=p[i];
	}
	cout<<ans-1;
	return 0;
}

小牛试刀

P6216 回文匹配

这题不是特别好像,考虑要想发挥出字符串真正的线性魅力,一般需要一些线性算法才能更加完美。

这题加的是二次前缀和

首先用 KMP算法 求出 \(s2\)\(s1\) 中出现的每一个位置,并准备第一次前缀和 \(s[i]\)(在每个出现位置的左端点 \(+1\))。其次,再用 Manacher 求一下每个回文半径。最后,发现其实这个答案形如 \(s[r]-s[l-1]+s[r-1]-s[l]+s[r-2]-s[l+1]+\dots\)

整理得 \(s[r]+s[r-1]+s[r-2]+\dots-s[l]-s[l+1]-s[l+2]-\dots\)

发现可以将前缀和数组再次进行前缀和来得到答案。

posted @ 2024-10-21 14:43  tyccyt  阅读(4)  评论(0编辑  收藏  举报