Manacher 学习笔记
1. Idea
用来处理一些有关回文串的问题,可以用优秀的 \(O(n)\) 时间求出原字符串串中任意一位为对称中心的奇数长度最长回文子串。
也正是因为只能求出奇数长度的最长回文子串,所以我们可以在每两位字符之间插入一个相同间隔符,这样以间隔符为对称中心的最长回文子串在原串中长度就是偶数的了。还可以在原串的最前面额外添加一个其它间隔符,这样就可以避免左右扩展求回文子串时还要特判当前是否超出边界。
先想想怎么暴力求出以第 \(i\) 位为对称中心的最长回文子串:从 \(i\) 同时向左右扩展,若相同,则将回文子串长度加 \(1\) 后继续扩展,否则停止。时间复杂度 \(O(n^2)\)。于是考虑优化这个暴力。
考虑从左到右处理原串中的每一位,设当前处理到 \(s_i\),记以前 \(i-1\) 位字符为对称中心的最长回文子串右端点的最大值为 \(r\),取到这个最大值的对称中心为 \(d\),以第 \(i\) 位为对称中心的最长回文半径为 \(p_i\)。
处理 \(s_i\) 时,若 \(r<i\),则直接从 \(s_i\) 暴力向左右扩展;若 \(r>i\),则说明 \(s_i\) 是右端点为 \(r\) 的以 \(d\) 为回文中心的最长回文子串的一部分。由于最长回文子串回文(废话),所以以 \(d\) 为对称中心的,\(d\) 左侧距离 \(d\) 这个字符 \(i-d+1\) 个字符的答案是可以被 \(i\) 参考继承的,由中点坐标公式得,这个字符为 \(s_{2d-i}\)。为什么说是参考呢?因为若以 \(s_{2d-i}\) 为对称中心的最长回文子串超出了以 \(d\) 为中心的最长回文子串的范围,那么超出的部分与 \(d\) 的右侧是不形成对称关系的。故可以将 \(p_i\) 设为 \(p_{2d-i}\) 和 \(r-i+1\) 的最小值,然后再继续向左右暴力扩展。
扩展完毕后,若此时右端点大于 \(r\),则更新 \(r\) 和 \(d\) 即可。
观察上述过程我们可以发现,Manacher 算法对于求最长回文子串的优化在于,它在求后面的子串时也用到了前面已经求出的最长回文子串辅助更新。
由于每次暴力左右扩展时,\(r\) 必然向右更新,所以均摊时间复杂度为 \(O(n)\)。
2. Example
2.1 【模板】manacher 算法
模板题,没什么好说的。
将每一位的最长回文子串长度取最大值即可,注意剔除间隔符的长度。
核心代码超短。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 22000005
int n,p[N],ans;
char in[N],s[N];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
int main(){
scanf("%s",in+1);
s[0]='!',s[1]='|',n=strlen(in+1);
for (int i=1;i<=n;i++) s[i<<1]=in[i],s[i<<1|1]='|';
for (int i=1,r=0,d=0;i<=n*2;i++){
p[i]=(r<i)?1:min(r-i+1,p[d*2-i]);
while (s[i-p[i]]==s[i+p[i]]) ++p[i];
if (i+p[i]-1>r) r=i+p[i]-1,d=i;
ans=max(ans,p[i]-1);
}
printf("%d\n",ans);
return 0;
}
2.2 P4555 [国家集训队]最长双回文串
这种原问题需要求一个,它却让加倍求两个的问题,就可以考虑对于每一个位置从前到后求一个,再从后到左求一个,然后将两个答案拼起来求最大值即可。(如求序列最大双段子段和等)。
本题也是一样,我们可以考虑分别求出以每一个位置为开头和结尾的最长回文子串长度,然后对于前后两个位置将答案拼起来取最大值。
那么怎么求呢?我们拿求以每一个位置为结尾的最长回文子串长度为例。
这个东西其实好求。考虑以 \(i+p_i-1\) 更新 \(r\) 时,以 \([r+1,i+p_i-1]\) 为结尾的最长回文子串的对称中心其实都为 \(i\)(仔细想想),故更新时顺便枚举这个区间的位置更新答案即可。
求以每一个位置为开头的最长回文子串长度同理,倒着求即可。
时间复杂度 \(O(n)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 200005
int n,a[N],b[N],m,p[N];
char s[N],in[N];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
int main(){
scanf("%s",in+1);
n=strlen(in+1);
s[0]='!',s[++m]='|';
for (int i=1;i<=n;i++) s[++m]=in[i],s[++m]='|';
for (int i=1,r=0,d=0;i<=m;i++){
p[i]=(r<i)?1:min(r-i+1,p[d*2-i]);
while (s[i-p[i]]==s[i+p[i]]) ++p[i];
if (i+p[i]-1>r){
d=i;
for (r=r+1;r<=i+p[i]-1;r++)
if (s[r]!='|') a[r]=r-i+1;
--r;
}
}
for (int i=m,r=m+1,d=m+1;i>=1;i--){
p[i]=(r>i)?1:min(i-r+1,p[d*2-i]);
while (s[i-p[i]]==s[i+p[i]]) ++p[i];
if (i-p[i]+1<r){
d=i;
for (r=r-1;r>=i-p[i]+1;r--)
if (s[r]!='|') b[r]=i-r+1;
++r;
}
}
int ans=0;
for (int i=1;i<=m-2;i++)
ans=max(ans,a[i]+b[i+2]);
printf("%d\n",ans);
return 0;
}
2.3. P1659 [国家集训队]拉拉队排练
当我们找到以每一个位置为对称中心的最长回文子串时,设该子串回文半径为 \(r\),则回文半径为 \([1,r-1]\) 的子串显然也是回文的。
所以我们可以开一个桶 \(s\),记 \(s_i\) 表示回文半径为 \(i\) 的回文子串的个数,初始时跑一遍 Manacher ,将以每一个位置为对称中心的最长回文子串放入桶中,然后从大到小扫描整个桶,若 \(s_i\) 有值,则将这些子串的答案统计到答案中,又由于这些子串的存在肯定意味着与他们个数相同的半径为 \(i-1\) 的回文子串也对应存在,故将这些个数再加入 \(s_{i-1}\) 中。直到当前统计了 \(k\) 个子串为止。
统计答案时若选择了 \(y\) 个长度为 \(x\) 的回文子串,则将答案乘上 \(x^y\),用快速幂处理即可。
注意由于题中限定了回文子串长度必须为奇数,所以就不需要加入分隔符了。
那么为什么这个做法可以在不找出所有回文子串的情况下还能找出前 \(k\) 长的回文子串呢?其实就是利用了部分回文子串之间的有序性,即对于一个对称中心 \(i\),若存在回文半径为 \(l\) 的回文子串,则一定存在长度为 \(l-1\) 的回文子串。所以对于每一个对称中心,其实就相当于已经开了一个隐式的堆。这个思想在 蚯蚓 那道题中可以深刻体现。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 2000005
const ll mod=19930726;
ll n,k,m,p[N],ans[N],anss=1,maxn;
char s[N];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
ll ksm(ll x,ll y){
ll ans=1,now=x;
while (y){
if (y&1) ans=ans*now%mod;
now=now*now%mod;
y>>=1;
}
return ans;
}
int main(){
n=read(),k=read();
scanf("%s",s+1);
s[0]='!';
for (ll i=1,r=0,d=0;i<=n;i++){
p[i]=(r<i)?1:min(p[d*2-i],r-i+1);
while (s[i+p[i]]==s[i-p[i]]) ++p[i];
if (i+p[i]-1>r) r=i+p[i]-1,d=i;
++ans[p[i]];
maxn=max(maxn,p[i]);
}
for (ll i=maxn;i>=1;i--){
if (k>ans[i]) anss=(anss*ksm(i*2-1,ans[i]))%mod,ans[i-1]+=ans[i],k-=ans[i];
else{
anss=(anss*ksm(i*2-1,k))%mod;
break;
}
}
printf("%lld\n",anss);
return 0;
}
2.4 P5446 [THUPC2018]绿绿和串串
考虑对于一个字符串,不难想到若存在某一个回文子串 \([l,r]\),对称中心为 \(d\),其中 \(r\) 为字符串最右端,那么将 \([1,d]\) 按照题目要求翻折过后,前 \(r\) 项一定为原字符串。
那么再来想,如果已知 \([1,d]\) 翻折过后是原字符串,那么如果存在一个回文子串 \([l',r']\),对称中心为 \(d'\),其中 \(r'\) 为 \(d\),那么如果将 \([1,d']\) 按照题目要求翻转,前 \(r\) 项一定也是原字符串……吗?有点问题,因为这样翻折我们只能保证前 \(r'\) 项和原串相同,而区间 \((r',r]\) 可能会因为和 \([1,l')\) 不对称而导致翻折后“串不对版”。由此还需要加一个限制条件,就是我们找到的二次回文子串,也就是右端点不为 \(r\) 的回文子串,左端点必须为 \(1\),这样才能保证翻折之后 \([1,l')\) 为空。
所以先跑一遍 Manacher,然后从右到左判断每个对称中心的回文子串是否满足条件,满足条件后将子串左端点标记以便于前面的子串进行判断。记录答案后从左到右输出即可。
时间复杂度 \(O(\sum |S|)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 1000005
int n,t,p[N],jl[N],jtot,ans[N],atot;
bool vis[N];
char s[N];
ll read(){
ll wh=0,fh=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') fh=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
wh=(wh<<3)+(wh<<1)+(c^48);
c=getchar();
}
return wh*fh;
}
int main(){
t=read();
while (t--){
scanf("%s",s+1);
n=strlen(s+1);
s[0]='!';
vis[n]=1,atot=jtot=0;
for (int i=1,r=0,d=0;i<=n;i++){
p[i]=(r<i)?1:min(r-i+1,p[d*2-i]);
while (s[i-p[i]]==s[i+p[i]]) ++p[i];
if (i+p[i]-1>r) r=i+p[i]-1,d=i;
}
for (int i=n;i>=1;i--){
if (vis[i+p[i]-1]&&(i==p[i]||i+p[i]-1==n)) ans[++atot]=i,vis[i]=1,jl[++jtot]=i;
}
for (int i=atot;i>=1;i--) printf("%d ",ans[i]);
puts("");
for (int i=1;i<=jtot;i++) vis[jl[i]]=0;
}
return 0;
}