Manacher 学习笔记
2021/02/23 看了 N 遍,终于看懂了。。。
推荐资料:OI-Wiki。
同时这篇博客还借鉴了洛谷春令营网课的资料,不得不说获益匪浅。
1 基本概念
1.1回文串
对于一个字符串 s [ 1 … n ] s[1\dots n] s[1…n],它是回文串当且仅当 ∀ i ∈ [ 1 , n ] , s [ i ] = s [ n − i + 1 ] \forall i\in[1,n],s[i]=s[n-i+1] ∀i∈[1,n],s[i]=s[n−i+1]。
1.2 回文子串
若 t t t 是 s s s 的子串且 t t t 是回文的,则称 t t t 是 s s s 的回文子串。
1.3 回文中心
奇回文串的中心是它最中心的那个位置,而偶回文串的中心是它最中间两个字符的间隔位置。
对于一个字符串,定义它一个(奇长度/偶长度)回文子串的回文中心为字符串的(奇回文中心/偶回文中心)
2 求解回文串的几种方法
我们有多种方法求解回文串,这里简单列举几种:
- 利用后缀数组的 h e i g h t \rm height height 数组:将原串翻转再对接,对每个位置求正反串的最长公共前缀。一般时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
- 字符串哈希,对每个位置 x x x 二分出最长的 r r r 使得 s [ x … x + r ] = s [ x − r … x ] s[x\dots x+r]=s[x-r\dots x] s[x…x+r]=s[x−r…x]。时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)。
- 接下来所说的 Manacher 算法。时间复杂度为 O ( n ) O(n) O(n)
3 Manacher 算法
3.1 一个巧妙的转化
为了避免奇回文中心和偶回文中心的讨论,我们在原串基础上增添一些字符。
d
b
a
a
b
a
b
⇒
&
#
d
#
b
#
a
#
a
#
b
#
a
#
b
#
d
b
a
a
b
a
b
⇒
&
#
d
#
b
#
a
#
a
#
b
#
a
#
b
#
或
&
#
d
#
b
#
a
#
a
#
b
#
a
#
b
#
\begin{aligned} &\tt dbaabab \\ \Rightarrow&\tt \&\#d\#b\#a\#a\#b\#a\#b\# \\ &\tt d{\color{Red}baab}ab \\ \Rightarrow&\tt\&\#d{\color{Red}\#b\#a\#a\#b\#}a\#b\# \\ \mathtt{或}&\tt\&\#d\#{\color{Red}b\#a\#a\#b}\#a\#b\# \end{aligned}
⇒⇒或dbaabab&#d#b#a#a#b#a#b#dbaabab&#d#b#a#a#b#a#b#&#d#b#a#a#b#a#b#
于是我们把奇回文与偶回文统一了。但我们发现原串的回文串和新串的回文串不一定是一一对应的。这没有关系,我们后文再说。
这个 & \tt \& & 的作用是什么呢?是为了防止匹配上头忘记停下来了。
3.2 朴素算法
我们考虑怎么求一个位置可以延伸出去的最长回文子串长度。定义 r ( x ) r(x) r(x) 表示使得 s [ x + t ] ≠ s [ x − t ] s[x+t]\ne s[x-t] s[x+t]=s[x−t] 的最小的 t t t。对每个点求解这个 r ( x ) r(x) r(x) 即可。时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
而 Manacher 其实是在朴素算法上做了个优化。
3.3 优化
情况1
比如一个字符串
x
y
z
d
a
b
a
a
a
b
a
e
f
e
a
b
a
a
a
b
a
d
x
y
z
\tt xyzdabaaabaefe{\color{green}aba}{\color{Red}a}{\color{green}aba}dxyz
xyzdabaaabaefeabaaabadxyz
我们要求它在
a
\tt \color{red} a
a 处的
r
r
r。
假如我们已经知道了
x
y
z
d
a
b
a
a
a
b
a
e
f
e
a
b
a
a
a
b
a
d
x
y
z
\tt xyz{\color{Green}dabaaabae}{\color{red}f}{\color{Green}eabaaabad}xyz
xyzdabaaabaefeabaaabadxyz
且
x
y
z
d
a
b
a
a
a
b
a
e
f
e
a
b
a
a
a
b
a
d
x
y
z
\tt xyzd{\color{Green}aba}{\color{red}a}{\color{Green}aba}efeabaaabadxyz
xyzdabaaabaefeabaaabadxyz
由对称性可直接推得这两个
a
\tt\color{red}a
a 的
r
r
r 是相同的。
情况2
类似于情况1:
x
a
b
c
b
a
e
f
e
a
b
c
b
a
e
f
y
x
a
b
c
b
a
e
f
e
a
b
c
b
a
e
f
y
{\tt x{\color{green}abcbae}{\color{red}f}{\color{green}eabcba}efy} \\ {\tt x{\color{green}ab}{\color{red}c}{\color{green}ba}efeabcbaefy}
xabcbaefeabcbaefyxabcbaefeabcbaefy
可直接推得
x
a
b
c
b
a
e
f
e
a
b
c
b
a
e
f
y
\tt xabcbaefe{\color{green}ab}{\color{red}c}{\color{green}ba}efy
xabcbaefeabcbaefy
但还没完!还可以继续扩展
x
a
b
c
b
a
e
f
e
a
b
c
b
a
e
f
y
\tt xabcbae{\color{green}feab}{\color{red}c}{\color{green}baef}y
xabcbaefeabcbaefy
3.4 Manacher 算法流程
Manacher算法是基于上面的优化的。假如我之前所求得的回文子串中可以延伸到的最大右边界为
m
=
x
+
r
(
x
)
m=x+r(x)
m=x+r(x),则如果现在循环到
i
<
m
i<m
i<m,则可以先设定
r
(
i
)
=
min
(
m
−
i
,
r
(
2
×
x
−
i
)
)
r(i)=\min(m-i,r(2\times x-i))
r(i)=min(m−i,r(2×x−i))
然后再暴力拓展。
上面那个式子,第一项是为了不超出“已知范围”,第二个式子则是对称性的约束。直接看上面两个优化就知道了。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
//char In[1 << 20], *ss = In, *tt = In;
//#define getchar() (ss == tt && (tt = (ss = In) + fread(In, 1, 1 << 20, stdin), ss == tt) ? EOF : *ss++)
ll read() {
ll x = 0, f = 1; char ch = getchar();
for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + int(ch - '0');
return x * f;
}
const int MAXN = 3e7 + 5;
char s[MAXN];
int n, r[MAXN];
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
for(int i = n; i >= 1; i--) s[i * 2 + 1] = s[i], s[i * 2 + 2] = '#';
s[1] = '@', s[2] = '#'; n = 2 * n + 2;
for(int c = 0, i = 1; i <= n; i++) {
if(i < c + r[c]) r[i] = min(c + r[c] - i, r[2 * c - i]);
while(i + r[i] <= n && s[i + r[i]] == s[i - r[i]]) r[i]++;
if(i + r[i] > c + r[c]) c = i;
}
int ans = 0;
for(int i = 1; i <= n; i++) ans = max(ans, r[i] - 1);
printf("%d\n", ans);
return 0;
}
3.5 Manacher算法的应用
求出每个点的 r r r 后,我们观察易知:
- 新串的最大回文子串长度为 max ( 2 r ( x ) − 1 ) \max(2r(x)-1) max(2r(x)−1),旧串的最大回文子串长度为 max ( r ( x ) − 1 ) \max(r(x)-1) max(r(x)−1)。
- 回文子串总个数为 ∑ x = 1 n ⌊ r ( x ) 2 ⌋ \sum_{x=1}^n\left\lfloor\dfrac {r(x)}{2}\right\rfloor ∑x=1n⌊2r(x)⌋。
由此可见,我们直接通过 r r r 推出这些性质,不需要原串与新串一一对应。