学习笔记: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\),有两种情况:

image-20250804133441169

第一种情况,\(R-i+1>d_{2c-i}\),此时 \(d_i\) 就等于 \(d_{2c-i}\),不可能进行扩张(否则与 \(d_{2c-i}\) 的最大性矛盾)。

image-20250804133454393

第二种情况,\(R-i+1\le d_{2c-i}\),此时绿色部分相等,但 \(>R\) 的部分不确定,故用朴素算法向外扩张。注意到此时如果扩张成功,则 \(R\) 一定增大。

洛谷-P3805 【模板】manacher 代码:

#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;
}

CodeForces-1080E Sonya and Matrix Beauty

posted @ 2026-09-10 16:46  f2021ljh  阅读(9)  评论(0)    收藏  举报