Manacher感性瞎扯

xtx1092515503

2020-01-16 11:39:21

Personal

Manacher=马拉车

大家好,我们今天来扯Manacher算法了。

I.马拉车可以干什么?

一句话:对于一个字符串s,在O(|S|)时间内,求出它的最长回文子串。

II.预处理

对于一个字符串,它的回文串可以有两种类型:

A.奇回文串

例:AACCBCCAA

特征:有单一回文中心(例中的B)。

B.偶回文串

例:AABBCCBBAA

特征:对称中心是两个(例中的BB)。

两者性质并不相同,必须分类讨论。

但是,我们有一种方法,可以将两者统一成一种类型:

在原串每两个字符之间,以及串头串尾,都加上一个无关字符(例如 # 等)。

例:

AABBCCDD\rightarrow #A#A#B#B#C#C#D#D#

ABCBA\rightarrow #A#B#C#B#A#

这样的话,原串中的奇回文串与偶回文串,都被统一成了奇回文串。(奇回文串变成以原串字符为对称中心的回文串,偶回文串变成以 # 为对称中心的回文串)。

代码(在读入时直接进行操作):

inline void getln(){
    s[0]='$',S=1;
    char c=getchar();
    while(c>'z'||c<'a')c=getchar();
    while(c>='a'&&c<='z')s[S++]=c,s[S++]='$',c=getchar();
    s[S]='\0';
}

III.主算法

(默认字符串从0开始)

(暂时忽略添加进来的 # 字符,因为忽略也无影响)

先考虑暴力思路:枚举对称中心,然后枚举对称半径。总复杂度O(n^2)

(当然,可以哈希+二分达到O(nlog_2n),但是这个思路对我们没有帮助)

同kmp算法一样,我们可以思考一些操作来避免重复枚举。

这时候我们就可以设两个变量:

例:**ABABAAC**,在位置$3$时,$Rpos=4$。在位置$2$和$3$的回文串都曾达到这里。 $Centre$:对于当前的$Rpos$,它所对应的对称中心。如果有多个,取最左端的一个。 例:**ABABAAC**,在位置$3$时,$Centre=2$。在位置$2$和$3$的回文串都曾达到$Rpos$,但是$2$是最左端的一个。 我们再设一个数组$rad$,表示位置$i$时的回文半径。 | 字母 | A | B | A | B | A | A | C | | :----------: | :----------: | :----------: | :----------: | :----------: | :----------: | :----------: | :----------: | | $rad_i$ | 1 | 2 | 3 | 2 | 1 | 1 | 1 | (自动忽略了偶回文串)。 接下来我们就开始操作了。 初始令$Rpos=Centre=-1$。 对于当前位置$i$: #### 1. $i \geq Rpos

暴力延伸,这里是未涉及到的新地方。

2. i < Rpos

这时,我们令refi关于Centre的对称点(即Centre*2-i)。

同时令LposRpos关于Centre的对称点。

再令lp为以ref为对称中心的回文串的左边界。

2.1. Lpos<lp

因为是回文串,所以有s_{Lpos}...s_{Centre}=s_{Rpos}...s_{Centre}

则关于ref对称的回文串,也是关于i对称的回文串(想一想,左右对称)。

如图:

2.2. Lpos\geq lp

对称的只有从iRpos的一段,剩下的部分两边是不同的,仍需暴力扩展。

如图:

IV.实现

inline void Manacher(){
    Centre=Rpos=-1;
    for(register int i=0;i<S;i++){
        rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):1;
        while(i+rad[i]<S&&i-rad[i]>=0)if(s[i+rad[i]]==s[i-rad[i]])rad[i]++;else break;
        if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i;
        Ans=max(Ans,rad[i]);
    }
}

实现与上面的描述很不一致,我们逐行分析。

rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):1;

用了三目运算符。

它的意思是:

如果i<Rpos,那么rad_i=min(Rpos-i,rad_{Centre*2-i} 回忆一下,ref就是Centre*2-i

$rad_{Centre*2-i}$,就是上文$2.1$中的可用部分。 两者取$min$,就很显然了。 而当$i \geq Rpos$时,$rad_i=1$(默认单个字符本身就为回文串)。 然后就是暴力跳了。 ```cpp while(i+rad[i]<S&&i-rad[i]>=0)if(s[i+rad[i]]==s[i-rad[i]])rad[i]++;else break; ``` 分析一下,在情形$1$和$2.2$,都需要暴力跳。在情形$2.1$,暴力跳一次就会退出。 然后按定义更新$Rpos$和$Centre$,同时取答案。 ```cpp if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i; Ans=max(Ans,rad[i]); ``` 最终答案为$Ans-1$,因为在长度为$n$的原串中加入了$n+1$个 **#** 。但是$rad$又是半径,也要再加入$rad-1$个字符才能构成回文串。 总复杂度$O(n)$。~~分析复杂度是不可能的,这辈子都是不可能的。~~ ### V.大礼包 **本文所有代码使用的分隔符都是dollar符号,但是由于$\LaTeX$的缘故,在讲解时使用 # 符号。另,还是因为$\LaTeX$,文中的dollar符号全部莫名其妙多了一个空格,请自行删除。** (代码:[【模板】manacher算法](https://www.luogu.com.cn/problem/P3805) ```cpp #pragma GCC optimise(3) #include<bits/stdc++.h> using namespace std; char s[22000100]; int S,rad[22001000],Centre,Rpos,Ans; inline void getln(){ s[0]='$',S=1; char c=getchar(); while(c>'z'||c<'a')c=getchar(); while(c>='a'&&c<='z')s[S++]=c,s[S++]='$',c=getchar(); s[S]='\0'; } inline void Manacher(){ Centre=Rpos=-1; for(register int i=0;i<S;i++){ rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):1; while(i+rad[i]<S&&i-rad[i]>=0)if(s[i+rad[i]]==s[i-rad[i]])rad[i]++;else break; if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i; Ans=max(Ans,rad[i]); } } int main(){ getln(); Manacher(); printf("%d\n",Ans-1); return 0; } ``` ~~(忽略上面的O3)~~ ### VI.例题 [ [POI2010]ANT-Antisymmetry](https://www.luogu.com.cn/problem/P3501) 题意:给你一个长度为$n$的$01$串,求它的非空并在异或意义下回文的子串数。 这里我们介绍马拉车的扩展: 引入$to$数组,表示每个字符与哪个字符匹配。 例如,在模板题中,有$\forall c \in ['a','z'],to_c=c$。 而在这道题中,有$to_{'1'}='0',to_{'0'}='1'

并且我们令to[#]=[#]

然后就可以匹配了。

注意!!这个时候初始回文串长度应为0,因为自己不一定与自己相配。

即:

rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):0;

最后是0不是1

另,统计答案时应是:

Ans+=(rad[i]>>1);

因为加入了辅助字符 # ,所以数量要减半。

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
int S,rad[1001000],Rpos,Centre,Ans;
char s[1001000],to[129];
void getln(){
    char c=getchar();
    to['0']='1',to['1']='0',to['$']='$';
    s[0]='$',S=1;
    while(c!='0'&&c!='1')c=getchar();
    while(c=='0'||c=='1')s[S++]=c,s[S++]='$',c=getchar();
    s[S]='\0';
}
void Manacher(){
    Rpos=Centre=-1;
    for(int i=0;i<S;i++){
        rad[i]=(i<Rpos)?min(Rpos-i,rad[Centre*2-i]):0;
        while(i-rad[i]>=0&&i+rad[i]<S)if(to[s[i-rad[i]]]==s[i+rad[i]])rad[i]++;else break;
        if(i+rad[i]>Rpos)Rpos=i+rad[i],Centre=i;
        Ans+=(rad[i]>>1);
    }
}
signed main(){
    scanf("%lld",&S);
    getln();
    Manacher();
    printf("%lld",Ans);
    return 0;
}

VII.总结

马拉车还是很妙的,总思想同也是求字符串匹配的Z算法类似。有兴趣的同学可以研究一下。

完结撒Manacher~~

(实在找不到Manacher本人图片了)