Manacher
\(Manacher\) 算法
引入:
先看两个字符串:
\(abccba\)
\(abcdcba\)
显然这两个字符串是回文的,但是两个串的对称中心的特性不同。
第一个串,它的对称中心是两个 \(c\) 中间,但是第二个串,对称中心就是 \(d\).
如果我们这样记录回文串的对称中心,就会复杂(因为可能不是字母)。
所以,为了解决这个问题,我们就在字母之间插入隔板,这样两个问题就统一了。
其实就是保证字符串除了首个字符,长度为奇数。
\(\sim |a|b|c|c|b|a|\)
这里我们还插入了一个 \(\sim\) , 原因在代码里说明。
特点-优秀的复杂度:
- 对于一个回文串,有且只有一个对称中心,我们叫他回文对称中心 \(MID\)。
- 在一个回文串内,任选一段区间 \(X\) ,一定存在关于 \(MID\) 对称的区间,叫做 关于区间 \(X\) 的对称区间
- 区间和对称区间全等
- 若一个区间的对称区间是回文串,这个区间一定是个回文串
- 通过确定关系预先得到的回文半径 \(R\),必定 \(R \leq r\) ,\(r\) 是这个位置的真实回文串半径。
因此,我们记录以每个位置为中心的回文串半径,通过 另一个回文中心 将这个 原先的中心点对称过去,可以确定对称过去那个点的回文半径了。
建立过程:
考虑 另一个回文中心 如何确定,就是那个极大回文串的回文中心,也就是边界顶着右边我们已知的最远位置的,最长的回文串。
我们只能确认我们已知的回文串内的对称关系和回文半径关系,不知道其右侧情况。
可以记录以 \(i\) 点为中心的回文半径到 \(P[i]\) 数组,同时记录:
\(mid\) :表示已经确定的右侧最靠右的回文串的对称中心
\(r\) : 右边界
当扫描到一个新字符,怎么确定部分回文半径:
若扫描位置为 \(i\) ,若 \(mid \leq i \leq r\) ,则我们可以找到它的一个对称点。
位置为: \(2* mid-i\) ,就是中点坐标公式的推导。
所以,拓展一个新点时,我们不用从这个点左右两边第一个位置开始拓展,可以预先确定一部分回文串,因此复杂度为线性。
若拓展一个新的关于该字符的回文半径,可以先确定一部分 \(P[i]\).
我们能够确定的范围,右侧不大于 \(r\) ,即:\(p[i]+i-1\leq r\) ,移项得到: \(P[i]\leq r-i+1\)
因此,\(p[i]\) 要取最小值:
p[i]=min(p[2*mid-i],r-i+1);
最终最长回文串长度就是 \(max_{i=1}^n P[i]-1\), 因为其插入字符 \(|\) 必定比其半径多 \(1\) ,因此要 \(-1\).
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1.1e7+5;
char s[N<<1];
int p[N<<1],cnt,ans;
void read(){
char c=getchar();
s[0]='~',s[cnt=1]='|';
while(c<'a'||c>'z') c=getchar();
while(c>='a'&&c<='z') s[++cnt]=c,s[++cnt]='|',c=getchar();
}
int main()
{
read();//读入,构造字符串
for(int i=1,r=0,mid=0;i<=cnt;i++){
// cout<<i<<endl;
if(i<=r) p[i]=min(p[(mid<<1)-i],r-i+1);//我们能够确定的范围
while(s[i-p[i]]==s[i+p[i]]) ++p[i];
//暴力拓展左右两侧,我们并不怕溢出,因为有 '~' 帮忙
//如果成功暴力拓展,意味着 r 单调递增,所以成线性。
if(p[i]+i>r) r=p[i]+i-1,mid=i;
//更新mid和r,保持r时最右的,保证我们提前确定的部分回文半径尽量多
if(p[i]>ans) ans=p[i];
}
cout<<ans-1<<endl;
system("pause");
return 0;
}
P4555 [国家集训队]最长双回文串
我们知道,\(P[i]\) 的含义是 \(i\) 点的最长回文半径。
因此,定义 \(L[i]\) 为以 \(i\) 结尾的最长回文子串长度,同理 \(R[i]\) 为以 \(i\) 开头的。
现在 \(manacher\) 模板里,处理出来这个回文子串的左右边界 \(i\), 表示向前有一个长度为 \(L[i]\) 的回文串,向后有一个长度为 \(R[i]\) 的回文串。
L[i+P[i]-1]=max(L[i+P[i]-1],P[i]-1);//维护左右边界
R[i-P[i]+1]=max(R[i-P[i]+1],P[i]-1);
然后进行递推:
因为 \(R[i]\) 是 \(i\) 结尾的回文长度,所以直接顺推,每往后移一位(实际串上为两位),最长回文子串长度 \(-2\)。
可以找个样例,输出一下 \(L[i],R[i]\) 的值理解一下,比较抽象。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
char s[N];
int P[N],cnt,ans=0,n,K,sum[N];
int L[N],R[N];//L表示以i为结尾的最长回文子串的长度,R表示以i为开头的最长回文子串的长度
void read(){
char c=getchar();
s[1]='~',s[cnt=2]='|';
while(c<'a'||c>'z') c=getchar();
while(c>='a'&&c<='z') s[++cnt]=c,s[++cnt]='|',c=getchar();
}
int main(){
read();
for(int i=1,mid=0,r=0;i<=cnt;i++){
if(i<=r) P[i]=min(P[(mid<<1)-i],r-i+1);
while(s[i-P[i]]==s[i+P[i]]) P[i]++;
if(P[i]+i>r) r=P[i]+i-1,mid=i;
L[i+P[i]-1]=max(L[i+P[i]-1],P[i]-1);//维护左右边界
R[i-P[i]+1]=max(R[i-P[i]+1],P[i]-1);
}
for(int i=2;i<=cnt;i+=2) R[i]=max(R[i],R[i-2]-2);
for(int i=cnt;i>=2;i-=2) L[i]=max(L[i],L[i+2]-2);
for(int i=2;i<=cnt;i+=2) if(L[i]&&R[i]) ans=max(ans,L[i]+R[i]);
cout<<ans<<endl;
system("pause");
return 0;
}