学习笔记:Manacher 算法
Manacher 算法
相关定义
如果字符串 \(s=s_1\cdots s_n\) 与其翻转 \(s'=s_n\cdots s_1\) 相等,则称 \(s\) 为 回文串。
设 \(n=|s|\),称 \(\lceil n/2\rceil\) 为 \(s\) 的 回文半径,\(s_{\lceil n/2\rceil}\) 和 \(s_{\lceil(n+1)/2\rceil}\) 为 \(s\) 的 回文中心。
\(s\) 是回文串当且仅当对于任意 \(1\le i\le n\) 有 \(s_i=s_{n-i+1}\)。
例如,回文串 \(s=\texttt{abacaba}\) 的回文中心为 \(s_4=\texttt c\),回文半径为 \(4\);回文串 \(s=\texttt{abba}\) 的回文中心为 \(s_2=s_3=\texttt b\),回文半径为 \(2\)。
利用字符串哈希,我们可以在 \(O(n)\) 时间内判断一个字符串是否是回文串。
Glenn K. Manacher 在 1975 年提出了 Manacher 算法,该算法用于计算一个字符串 \(s\) 的 所有回文子串。
尽管最多可能有 \(O(n^2)\) 个回文子串,但 Manacher 算法的复杂度是 \(O(n)\) 的。
设以 \(s_i\) 为回文中心的回文子串的最大半径为 \(d_i\)。我们的目标是求出所有 \(d_i\)。
统一奇偶回文子串
为了避免分类讨论,将所有偶数长度子串都变成奇数长度的子串:在 \(s\) 的每两个字符之间插入一个字符 \(\texttt\#\)。
例如将 \(s=\texttt{abcbbc}\) 变为 \(s=\texttt{\#a\#b\#c\#b\#b\#c\#}\),极长回文子串 \(\texttt{bcb}\) 变为 \(\texttt{\#b\#c\#b\#}\),\(\texttt{cbbc}\) 变为 \(\texttt{\#c\#b\#b\#c\#}\)。
即所有子串的长度由 \(l\) 变为 \(2l+1\)。
算法过程
显然,我们有 \(O(n^2)\) 的朴素算法:对于每个 \(i\),以 \(i\) 为中心向左右尽量扩张,直到字符不相等为止。
Manacher 算法(以及各种字符串算法)的精髓就在于,如何利用已知的信息,简化枚举的次数。
对于当前 \(i\),如果有 \(c<i\),使得 \(r_c=c+d_c-1>i\),那么 \(i\) 是回文串 \(s[c-d_c+1..c+d_c-1]\) 的一部分,与 \(i\) 对称的是 \(2c-i\)。维护 \(R=\max_{c\le i}\{r_c\}\) 及其对应的 \(c\),有两种情况:
第一种情况,\(R-i+1>d_{2c-i}\),此时 \(d_i\) 就等于 \(d_{2c-i}\),不可能进行扩张(否则与 \(d_{2c-i}\) 的最大性矛盾)。
第二种情况,\(R-i+1\le d_{2c-i}\),此时绿色部分相等,但 \(>R\) 的部分不确定,故用朴素算法向外扩张。注意到此时如果扩张成功,则 \(R\) 一定增大。
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
signed main() {
cin.tie(0)->sync_with_stdio(false);
string s, t = "";
int n, m, ans = 0;
cin >> s;
n = s.size();
t.pb('!'), t.pb('@');
for (int i = 0; i < n; i++) t.pb(s[i]), t.pb('@');
t.pb('#');
m = t.size();
vector<int> d(m + 1, 0);
for (int i = 1, c = 0, R = 0; i < m; i++) {
d[i] = R < i ? 1 : min(d[c * 2 - i], R - i + 1);
while (t[i - d[i]] == t[i + d[i]]) ++d[i];
ans = max(ans, d[i] - 1);
if (R - i + 1 < d[i]) c = i, R = i + d[i] - 1;
}
cout << ans << '\n';
return 0;
}
当然,利用字符串哈希,我们可以用二分在 \(O(\log n)\) 时间内求出任意 \(d_i\),预处理哈希时间 \(O(n)\)。
例题
洛谷-P1659 [国家集训队] 拉拉队排练
给定一个字符串 \(s\),求所有长度为奇数的回文子串中,前 \(K\) 长的长度的乘积,对 \(19930726\) 取模。如果不足 \(K\) 个,输出 \(-1\)。
\(1\le n\le10^6\),\(1\le K\le10^{12}\)。\(s\) 由小写字母组成。
跑 Manacher 获得 \(d_i\) 之后,存在长度为 \(1,3,\cdots,d_i\) 的回文子串,相当于在值域数组上连续加,用差分维护即可。
最后跑一遍前缀和,再用快速幂求积,总复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
int const MOD = 19930726;
int const N = 1e6 + 10;
int n, m, R[N << 1], a[N];
char s[N], t[N << 1];
long long k;
int ans = 1;
int mul(int a, int b) { return (1ll * a * b >= MOD) ? (1ll * a * b % MOD) : (a * b); }
int mulEq(int &a, int b) { return a = mul(a, b); }
int ksm(int p, int x) {
if (p == 1) return 1;
int res = 1;
while (x) {
if (x & 1) mulEq(res, p);
mulEq(p, p);
x >>= 1;
}
return res;
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n >> k;
cin >> s + 1;
n = strlen(s + 1);
t[0] = '!', t[++m] = '#';
f(i, 1, n) t[++m] = s[i], t[++m] = '#';
t[++m] = '@';
for (int i = 1, c = 0, r = 0; i < m; ++i) {
R[i] = i > r ? 1 : min(R[2 * c - i], r - i + 1);
while (t[i - R[i]] == t[i + R[i]]) ++R[i];
if (i + R[i] - 1 > r) c = i, r = i + R[i] - 1;
}
vector<int> d(n + 1, 0);
f(i, 1, n) if (!(R[2 * i] & 1)) {
int r = R[2 * i] - 1;
++d[1], --d[(r + 1 >> 1) + 1];
}
f(i, 1, n / 2 + 1) d[i] += d[i - 1];
g(i, n / 2 + 1, 2) {
if (d[i] >= k) {
mulEq(ans, ksm((i << 1) - 1, k));
cout << ans << '\n';
return 0;
} else {
mulEq(ans, ksm((i << 1) - 1, d[i]));
k -= d[i];
}
}
if (k > n) cout << "-1\n";
else cout << ans << '\n';
return 0;
}
*洛谷-P4555 [国家集训队] 最长双回文串
给定长度为 \(n\) 的字符串 \(S\),求 \(S\) 的最长双回文子串 \(T\) 的长度,其中双回文串是指可以拆分成两个回文串的字符串。
\(2\le n\le10^5\)。
利用 Manacher,我们可以求出以字符 \(s_i\) 结尾的最长回文子串长度 \(rt_i\):
- 当求出 \(d_i\) 时,如果要更新 \(R\) 为 \(i+d_i-1\),则对于 \(r\in[R+1,i+d_i-1]\),有 \(rt_r=r-i+1\)。
在扩张的同时计算即可。同理,倒着跑一遍可以求出以字符 \(s_i\) 开头的最长回文子串长度 \(lf_i\)。进而可求出答案。
(用 vector resize 函数不知道为什么 RE 了,遂改成静态数组。。。)
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
int const N = 1e5 + 10;
int n, m, d[N << 1], lf[N], rt[N], ans;
string s, t;
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> s;
n = s.size(), m = 2 * n + 3, ans = 0;
t.pb('!'), t.pb('@');
for (int i = 0; i < n; i++) t.pb(s[i]), t.pb('@');
t.pb('#');
for (int i = 1, c = 1, R = 0; i < m; i++) {
if (R < i) {
d[i] = 1;
R = c = i;
rt[i / 2] = 1;
} else if (R - i + 1 > d[c * 2 - i]) {
d[i] = d[c * 2 - i];
continue;
} else d[i] = R - i + 1;
while (t[i - d[i]] == t[i + d[i]]) {
++d[i], ++R;
if (!(R & 1)) {
rt[R / 2] = d[i];
}
}
c = i;
}
reverse(t.begin(), t.end());
memset(d, 0, sizeof(d));
for (int i = 1, c = 1, R = 0; i < m; i++) {
if (R < i) {
d[i] = 1;
R = c = i;
lf[i / 2] = 1;
} else if (R - i + 1 > d[c * 2 - i]) {
d[i] = d[c * 2 - i];
continue;
} else d[i] = R - i + 1;
while (t[i - d[i]] == t[i + d[i]]) {
++d[i], ++R;
if (!(R & 1)) {
lf[R / 2] = d[i];
}
}
c = i;
}
for (int i = 1; i < n; i++) {
ans = max(ans, rt[i] + lf[n - i]);
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号