Manacher

\(Manacher\) 算法

引入:

先看两个字符串:

\(abccba\)

\(abcdcba\)

显然这两个字符串是回文的,但是两个串的对称中心的特性不同。

第一个串,它的对称中心是两个 \(c\) 中间,但是第二个串,对称中心就是 \(d\).

如果我们这样记录回文串的对称中心,就会复杂(因为可能不是字母)。

所以,为了解决这个问题,我们就在字母之间插入隔板,这样两个问题就统一了

其实就是保证字符串除了首个字符,长度为奇数。

\(\sim |a|b|c|c|b|a|\)

这里我们还插入了一个 \(\sim\) , 原因在代码里说明。

特点-优秀的复杂度:

  1. 对于一个回文串,有且只有一个对称中心,我们叫他回文对称中心 \(MID\)
  2. 在一个回文串内,任选一段区间 \(X\) ,一定存在关于 \(MID\) 对称的区间,叫做 关于区间 \(X\) 的对称区间
  3. 区间和对称区间全等
  4. 若一个区间的对称区间是回文串,这个区间一定是个回文串
  5. 通过确定关系预先得到的回文半径 \(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;
}
posted @ 2021-09-24 16:41  Evitagen  阅读(41)  评论(0编辑  收藏  举报