1. 前言
Manacher 算法,俗称“马拉车”算法,是一种字符串算法,该算法可以在线性时间内求出一个串中最长回文串的长度,以及以每一个点为回文中心的奇长度回文串的长度。
实际上偶长度回文串的长度也能够求,后面会讲。
前置知识:无。
2. 详解
2.1 奇偶化归
上面这个词是我自己编的,百度搜不到qwq
首先看这两个回文串:ABABABA
和 ABAABA
。
前面的回文串长度为 7,是个奇数,这样的回文串称之为奇长度回文串,简称奇回文串。
后面的回文串长度为 6,是个偶数,这样的回文串称之为偶长度回文串,简称偶回文串。
可以发现,奇回文串的回文中心是最中间的字符,偶回文串的回文中心是中间两个字符的中间(也就是 AA
中间的空隙)。
因为奇回文串与偶回文串回文中心性质不相同(一个字符,一个空隙),因此我们需要对字符串做一点手脚:在每两个字符中间以及左右两端插入一个未曾出现过的字符。
比如说还是上面两个字符串 ABABABA
和 ABAABA
,我们按照上述描述插入 #
这个字符,于是这两个字符串就变成了 #A#B#A#B#A#B#A#
和 #A#B#A#A#B#A#
。
此时你会发现这两个字符串统一变成了奇回文串。
因此 Manacher 算法的第一步就是插入字符,使所有可能回文串变成奇回文串。
我将其称之为『奇偶化归』,因为这个方法统一了奇回文串和偶回文串。
下面若无特殊说明,默认字符串为奇偶化归之后的字符串。
2.2 翻转推移
还是我自己起的qwq
实际上翻转推移分为 2 块:翻转、推移。
接下来是 Manacher 算法的核心步骤。
设 f i f_i fi 表示以 i i i 为回文中心的字符串的最长半径(不是长度)。
又设两个值 i d , M a x n id,Maxn id,Maxn,其中 M a x n Maxn Maxn 是所有回文中心在 [ 1 , i − 1 ] [1,i-1] [1,i−1] 中的回文串所能到达的最右端距离的最大值,而 i d id id 是这个回文中心。我称这个字符串为最右字符串。
显然,以 i i i 为回文中心,这个回文串能够到达的最右端距离是 f i + i f_i+i fi+i。
那么假设我们已经处理完了 [ 1 , i − 1 ] [1,i-1] [1,i−1] 内的所有 f i f_i fi,如何处理 f i f_i fi 呢?
看图。
- 如果 i < M a x n i<Maxn i<Maxn。
设在以 i d id id 为中心的回文串中 i i i 的对称位置为 j j j,那么由中点公式有 j = 2 × i d − i j=2 \times id-i j=2×id−i。
此时考虑两种情况:
- 若以 j j j 为回文中心的回文串在最右字符串中间,显然有 f i = f j f_i=f_j fi=fj。
- 若不是,继续看下图:
上图的红色部分代表以 j j j 为中心的回文串。
显然,上图的两个紫色部分代表的字符串相同,而且互相全等,此时的回文半径为 M a x n − i Maxn-i Maxn−i。
那么因此有 f i = M a x n − i f_i=Maxn-i fi=Maxn−i。
两者取 min \min min 即可得到 f i f_i fi 的初值。
- 若 i ≥ M a x n i \geq Maxn i≥Maxn
这个时候我们什么也不知道,只能令 f i = 1 f_i=1 fi=1。
综上,当 i < M a x n i <Maxn i<Maxn 有 f i = min ( f j , M a x n − i ) f_i=\min(f_j,Maxn-i) fi=min(fj,Maxn−i)。
当 i ≥ M a x n i \geq Maxn i≥Maxn 有 f i = 1 f_i=1 fi=1。
上述操作就是『翻转』操作,因为其充分利用了回文串的性质,对称翻转得到了尽可能大而又准确的 f i f_i fi。
但显然这个 f i f_i fi 肯定是有问题的,因为当 f i = 1 f_i=1 fi=1 或 f i = M a x n − i f_i=Maxn-i fi=Maxn−i 时可能会有更长的回文半径。
此时我们就需要推移得到真正的 f i f_i fi,这一块 暴力 做即可。
我没骗你,暴力,而且复杂度是正确的,就是 O ( n ) O(n) O(n) 做法。
暴力做法就是直接暴力匹配 i + f i i+f_i i+fi 与 i − f i i-f_i i−fi,看看能不能继续向外扩展回文串即可。
最后不要忘记更新 i d , M a x n id,Maxn id,Maxn。
上述做法我将其称之为『推移』操作,因为这个操作将 M a x n Maxn Maxn 往右边推移了。
那么最长回文串长度就是
max
{
f
i
}
−
1
\max\{f_i\}-1
max{fi}−1,即最大半径 -1,减一是因为一个要去除我们奇偶化归时加入的字符 #
,另一个没有除以 2 是因为本身我们的字符串长度就是翻倍过的。
2.3 复杂度证明
上面提到了,有一个暴力推移操作,这个操作后面的部分很暴力,还能做到开头提出的 O ( n ) O(n) O(n) 算法吗?
复杂度仍然是 O ( n ) O(n) O(n) 的,证明如下:
考虑 f i f_i fi 初值的 3 种情况:
- f i = f j f_i=f_j fi=fj。
此时因为 f j f_j fj 已经达到最大,那么 f i f_i fi 也达到最大,因此无法往外推移,不计复杂度。
- f i = M a x n − i / f i = 1 f_i=Maxn-i/f_i=1 fi=Maxn−i/fi=1
这个时候就需要推移了。
但是需要注意的是,对于前一种情况,以 i i i 为回文中心的回文串最右端就是 M a x n Maxn Maxn,而对于后一种情况, i i i 本身大于 M a x n Maxn Maxn。
因此在这两种情况下, M a x n Maxn Maxn 一定会变大,而且也只能变大。
因为 M a x n Maxn Maxn 最大值为 n n n(整个串就是回文串),而 M a x n Maxn Maxn 只能往右边推移,因此更新 M a x n Maxn Maxn 也就是暴力的复杂度至多为 O ( n ) O(n) O(n)。
综上,暴力总复杂度为 O ( n ) O(n) O(n),而且推移部分不会与暴力部分干扰。
这就说明了其实推移部分的 O ( n ) O(n) O(n) 与暴力部分的 O ( n ) O(n) O(n) 复杂度是独立的,因此总复杂度是相加不是相乘,为 O ( n ) O(n) O(n)。
2.4 代码
请注意代码里面的枚举细节,
i
i
i 是从字符串的第二个字符开始枚举的,如果从第一个字符开始枚举,会导致 i - f[i]
越界,造成代码错误。
代码:
/*
========= Plozia =========
Author:Plozia
Problem:P3805 【模板】manacher算法
Date:2021/5/12
========= Plozia =========
*/
#include <bits/stdc++.h>
#define Max(a, b) (((a) > (b)) ? (a) : (b))
typedef long long LL;
const int MAXN = 2.3e7 + 10;
int len1, len2, f[MAXN];
char str1[MAXN], str2[MAXN];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return sum * fh;
}
// int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }
void init()
{
len1 = strlen(str1);
str2[0] = '^'; str2[1] = '$';
for (int i = 0; i < len1; ++i)
{
str2[(i << 1) + 2] = str1[i];
str2[(i << 1) + 3] = '$';
}
str2[(len1 << 1) + 2] = '@';
len2 = (len1 << 1) + 2;
}
void Manacher()
{
int id = 0, Maxn = 0;
for (int i = 1; i < len2; ++i)
{
if (Maxn > i) f[i] = Min(f[(id << 1) - i], Maxn - i);
else f[i] = 1;
for (; str2[i + f[i]] == str2[i - f[i]]; ++f[i]) ;
if (f[i] + i > Maxn) { Maxn = f[i] + i; id = i; }
}
}
int main()
{
scanf("%s", str1);
init(); Manacher(); int ans = 0;
for (int i = 0; i <= len2; ++i) ans = Max(ans, f[i]);
printf("%d\n", ans - 1);
}
3. 总结
Manacher 算法分为 2 步:奇偶化归,翻转推移。
- 奇偶化归:将奇回文串与偶回文串化归为奇回文串。
- 翻转推移:利用回文串性质翻转得到 f i f_i fi 初值,暴力推移得到正确的 f i f_i fi。