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}\)。也就是最长的回文半径:
同时附上 \(d'_i\) 的定义(偶数长度回文串半径):\(d'_i=\max\{x\},\forall 0<y\le x,s_{i-y}=s_{i+y-1}\)。
如何求出数组 \(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\),只需要改一些表达式即可,具体见代码。
代码
#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\) 每个字符见加上分隔号,如:
我们会惊奇的发现:变换后没有偶数长度的回文串!
证明:不存在 \(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;
}