Manacher

本文建议进行适当缩小以适应 \(\KaTeX\) 行间公式。

由于一些奇怪的原因,不等号(\(\ne\))无法正确渲染。

\(s[i\dots j]\) 表示 \(s\)\(i\)\(j\) 的子串。

\(s\) 的长度为 \(n\),即 \(|s|=n\)

默认字符串下标从 \(0\) 开始。

建议:前置知识(exKMP)(真的很像)。

引入

回文串的定义是满足 \(s_i=s_{n-i-1}\) 的字符串 \(s\)。通俗来说,就是正着写反着写一样的字符串。如:\(\texttt{abba},\texttt{abcba},\texttt{a},\texttt{aa}\) 都是。

那么如何求一个字符串 \(s\) 的回文子个数?啊哈哈哈哈,Manacher 来咯

Manacher

明显一个字符串是回文串有两种:长度为奇数、长度为偶数。我们这里先讨论长度为奇数的回文子串个数。(长度为偶数的回文子串原理相同,代码改一下就可以了。)

考虑到回文子串可能一共有 \(O(n^2)\) 个,一个个验证不现实。但好在可以观察到一个性质:若 \(s[l\dots r]\) 是回文子串且 \(l+1\le r-1\)\(s[l+1\dots r-1]\) 也是。正确性显然:只是各删掉了头尾的一个字符,仍然回文。

维护数组 \(d_i=\max\{x\}\),其中 \(x\) 满足 \(\forall 0<y\le x,s_{i-y+1}=s_{i+y-1}\)。也就是最长的回文半径:

\[\texttt{a}\overbrace{\texttt{ba}\underset{s_i}{\texttt{b}}\texttt{ab}}^{d_i=3}\texttt{c} \]

同时附上 \(d'_i\) 的定义(偶数长度回文串半径):\(d'_i=\max\{x\},\forall 0<y\le x,s_{i-y}=s_{i+y-1}\)

\[\texttt{a}\overbrace{\texttt{ba}\underset{s_i}{\texttt{a}}\texttt{b}}^{d'_i=2}\texttt{c} \]

如何求出数组 \(d_i\) 呢?明显需要利用以前计算出的 \(d\) 值。

依次从左往右计算 \(d_i\):维护一个区间 \([l,r]\) 满足 \(s[l\dots r]\) 回文且 \(r\) 最大(当然是已经求出的,即 \(\cfrac{l+r}{2}<i\),其中 \(\cfrac{l+r}{2}\) 即回文中心)。初始时 \(l=r=0\)

分类讨论:

  • \(r<i\),回文区间 \([l,r]\) 就没有利用价值了,此时暴力向两边枚举 \(x\),计算 \(d_i\) 大小。

  • 反之 \(i\le r\),因为回文串左右对称,必然存在 \(i\) 的对称点 \(j=l+r-i\ne i\),而 \(d_j\) 已经被计算过。

    此时我们有需要分类讨论了:

    • 如果 \(j-d_j<l\),即以 \(j\) 为中心的回文串突到了 \([l,r]\) 外面(包括 \(j-d_j+1=l\),即顶着边界):

      \[s_0\dots \underbrace{s_{j-d_j+1}\dots s_{l-1}}_{\text{\color{blue}these}}~\overbrace{\underbrace{s_l\dots {\color{red}s_j}\dots s_{j+d_j-1}}_{\text{\color{red}these}}\dots \underbrace{s_{i-d_j+1}\dots{\color{red}s_i}\dots s_r}_{\text{\color{red}these}}}^{[l,r]}~s_{r+1}\dots \]

      显然“\(\text{\color{blue}these}\)”段被截断在 \([l,r]\) 外,\([l,r]\) 只能保证两个“\(\text{\color{red}these}\)”段相等(因为对称)。

      我们只能置 \(d_i\gets r-i+1\),然后暴力增加 \(d_i\)

    • 反之如果 \(j-d_j\ge l\),也就是没有突出去也没有顶着边界:

      \[\small s_0\dots s_{l-1}~\overbrace{s_l\dots {\color{blue}s_{j-d(j)}}~\underbrace{s_{j-d(j)+1}\dots {\color{red}s_j}\dots s_{j+d(j)-1}}_{\text{\color{red}these}}~{\color{green}s_{j+d(j)}}\dots {\color{green}s_{i-d(j)}}~\underbrace{s_{i-d(j)+1}\dots{\color{red}s_i}\dots s_{i+d(j)-1}}_{\text{\color{red}these}}~{\color{blue}s_{i+d(j)}}\dots s_r}^{[l,r]}~s_{r+1}\dots \]

      显然两个“\(\text{\color{red}these}\)”段相等(还是因为对称)。

      然鹅因为以 \(s_j\) 为中心的回文子串的扩展结束于 \({\color{blue}s_{j-d(j)}},{\color{green}s_{j+d(j)}}\),故它俩不相等(\({\color{blue}s_{j-d(j)}}\ne{\color{green}s_{j+d(j)}}\))。

      因为对称,\({\color{blue}s_{j-d(j)}}={\color{blue}s_{i+d(j)}},{\color{green}s_{j+d(j)}}={\color{green}s_{i-d(j)}}\)

      由上两行可得 \({\color{blue}s_{i-d(j)}}\ne{\color{green}s_{i+d(j)}}\)。即扩展会在它俩出停止,所以 \(d_i=d_j\)

合并逻辑:

  • \(i\le r\land j-d_j\ge l\)\(d_i=d_j\)
  • 否则 \(d_i\gets\max(0,r-i+1)\),暴力扩展 \(d_i\)

是不是十分简洁?如果求 \(d'_i\),只需要改一些表达式即可,具体见代码。

代码

题目:P3805 【模板】manacher - 洛谷

#include <iostream>
using namespace std;

const int N=11451419;
string s;
int d[N],d_[N];
int n;

void Manacher()
{
	d[0]=1;
	for(int i=1,j,l=0,r=0;i<n;i++)
	{
		j=l+r-i;
		if(i<=r&&j-d[j]>=l)
		{
			d[i]=d[j];
		}
		else
		{
			d[i]=max(1,r-i+1);
			while(0<=i-d[i]&&i+d[i]<n&&s[i-d[i]]==s[i+d[i]])
			{
				d[i]++;
			}
			if(i+d[i]-1>r)
			{
				l=i-d[i]+1;
				r=i+d[i]-1;
			}
		}
	}
	d_[0]=0;// 初始化与奇数回文串不同
	for(int i=1,j,l=0,r=0;i<n;i++)
	{
		j=l+r-i+1;// 对称点不同,可以画图理解
		if(i<=r&&j-d_[j]>l)
		{
			d_[i]=d_[j];
		}
		else
		{
			d_[i]=max(0,r-i+1);
			while(0<=i-d_[i]-1&&i+d_[i]<n&&s[i-d_[i]-1]==s[i+d_[i]])// 暴力也不同
			{
				d_[i]++;
			}
		}
		if(i+d_[i]-1>r)
		{
			l=i-d_[i];// 回文边界不同
			r=i+d_[i]-1;
		}
	}
	return;
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>s;
	n=s.length();
	Manacher();
	int ans=-1;
	for(int i=0;i<n;i++)
	{
		ans=max(ans,max(2*d[i]-1,2*d_[i]));// 统计答案:最长回文子串长度
		// printf("%d:%d,%d\n",i,d[i],d_[i]);
	}
	printf("%d",ans);
	return 0;
}

巧妙的合并

上面的代码是不是很长?而且有许多烦人的奇偶性问题,能不能合并奇数长度、偶数长度的回文串计算?

可以!考虑将计算的字符串 \(s\) 每个字符见加上分隔号,如:

\[\texttt{ababba}\to\texttt{\#a\#b\#a\#b\#b\#a\#} \]

我们会惊奇的发现:变换后没有偶数长度的回文串!

证明:不存在 \(l,r\) 使得子串 \(s[l\dots r]\) 回文且 \(r-l+1\) 为偶数。

\(\because\) \(r-l+1\) 为偶数

\(\therefore\) \(s_l=\#,s_r\ne\#\)\(s_l\ne\#,s_r=\#\)(看图可知)

\(\therefore\) \(s_l\ne s_r\)

\(s[l\dots r]\) 不是回文串,与已知矛盾,得证。

那么我们将所求字符串变换后,就可以直接计算奇数长度回文串啦!

注意计算出的 \(d_i\) 统计答案时需要减 \(1\)(观察图)。

代码

#include <iostream>
using namespace std;

const int N=11451419;
string s;
int d[N*2];
int n;

void Manacher(string &t)
{
	d[0]=1;
	for(int i=1,j,l=0,r=0;i<n;i++)
	{
		j=l+r-i;
		if(i<=r&&j-d[j]>=l)
		{
			d[i]=d[j];
		}
		else
		{
			d[i]=max(1,r-i+1);
			while(0<=i-d[i]&&i+d[i]<n&&t[i-d[i]]==t[i+d[i]])
			{
				d[i]++;
			}
		}
		if(i+d[i]-1>r)
		{
			l=i-d[i]+1;
			r=i+d[i]-1;
		}
	}
	return;
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>s;
	n=s.length();
	string t(2*n+1,'#');
	for(int i=0;i<n;i++)
	{
		t[2*i+1]=s[i];
	}
	n=2*n+1;
	Manacher(t);
	int ans=-1;
	for(int i=0;i<n;i++)
	{
		ans=max(ans,d[i]-1);// 统计答案:最长回文子串长度
	}
	printf("%d",ans);
	return 0;
}
posted @ 2023-11-17 21:19  Po7ed  阅读(8)  评论(0编辑  收藏  举报