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 @   Po7ed  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示