Skip to content

manacher

由于 4 年半前(惊了,居然已经过了这么久了吗 QAQ)学过一次,所以这严格上来说应该是 Re: 从零开始的 mancher 重拾笔记(话说我怎么和初学的状态一样啊)

Introduction

我们还是套路式的从 intro 开始吧(

如果想要单独求出某一个回文串,显然弄两个指针 \(\mathcal{O}(n)\) 比较一遍就好了,但是如果要求出所有的字符串呢?最容易想到的一个优化就是,只要求出了以下标 \(i\) 为中心的最长回文串,那么我们其实也就求出了以下标 \(i\) 为中心的所有回文串。在考虑了这个优化下的暴力算法复杂度是 \(\mathcal{O}(n^2)\) 的,但是这还是不够优秀。

如果使用字符串哈希呢?仍然采用上面的 trick,要求最长的长度我们就再套一个二分,这样可以在 \(\mathcal{O}(n\log n)\) 的时间内求出来。

但是啊,这玩意有一个很简单的算法可以在 \(\mathcal{O}(n)\) 的时间内求出来,那就是 manacher 算法(点题!)。道理和 KMP 是类似的,充分利用回文串的性质,需要有强大的注意力

Manacher

过程很简单,由于偶数长度的回文串的求解可以通过在原字符串中添加~奇奇怪怪的~字符转化为对奇数长度的回文串的求解,因此下面只考虑对奇数长度的回文串的求解。

首先,我们维护一个右端点具有最大下标的回文串的中心下标 \(id\) 和右端点下标 \(r\),记 \(d[i]\) 表示以下标 \(i\) 为中心的最长奇数长度回文串的半径大小。

枚举中心 \(i\),假如当前枚举的下标 \(i<r\),那么由回文串的性质,将 \(i\)\(id\) 为中心对称过去得到对应的下标 \(j=id*2-i\),则几乎可以认为 \(d[i]=d[j]\),在下图中这是很显然的(图片来自 oi-wiki):

不过考虑到 \(j\) 的最长回文串的左端点可能落在 \(id\) 的最长回文串之外,因此我们需要砍掉多出去的部分,然后再暴力向外扩展 \(d[i]\)

另一种情况是,如果当前枚举的下标 \(i\ge r\),那么就直接暴力枚举。

最后都要记得用 \(i\) 的结果更新 \(id\)\(r\)

Complexity

在以上过程中,只要 \(i\) 的最长回文串是完整落在 \(id\) 的最长回文串内部的,那么对于 \(i\) 的求解是 \(\mathcal{O}(1)\) 的,而一旦 \(i\) 的最长回文串落在了 \(id\) 的最长回文串外,我们只会枚举落在外面的部分,并且枚举完成后会立即更新 \(id\)\(r\),也就是说每个下标最多只会被枚举两次,一次是成为左端点,另一次是成为右端点,所以 manacher 算法是 \(\mathcal{O}(n)\) 的。

Some Naive Thoughts

总觉得这种字符串中 KMP 和 manacher 解决的问题和对数组进行排序的问题有点像(强行联想),但是为什么字符串中这两个算法都能做到 \(\mathcal{O}(n)\) 而排序的上限平均就是 \(\mathcal{O}(n\log n)\) 的呢?

想起之前在知乎上刷到过为什么排序算法的上限是 \(\mathcal{O}(n\log n)\) 的问题,底下关于信息论的回答令我大呼妙哉,居然还有这种角度?!自此之后,信息论成为了我心中的一本圣经,总觉得这玩意简直就是这世界的真理,它可以回答任何问题,可以求出任何优化的极限,用信息论看问题简直和开了神之眼俯视世界一样。

我们再回到这个例子,用(我以为的)信息论来考虑,那一定是 \(d[]\)\(next[]\) 数组相比于排序问题仅仅提供了大小关系,他们提供了更丰富的信息来降低不确定性,信息量更大,因此优化的理论极限复杂度会更低。话说这不是扯了一堆最后结论是句废话嘛

但是嘛,一直没找到时间好好学学信息论,所以这仅仅是一些「naive thoughts」QwQ

有空的时候一定会认真学信息论的(确信

Problems

板子就不特意放上来了,仅仅处理奇数长度回文串的板子和使用 trick 求出奇偶长度回文串的板子都在这两题里有体现。

「Luogu P4555」[国家集训队]最长双回文串

Luogu

递推求出下标 \(i\) 分别作为回文串(不一定是 manacher 中的最长回文串)的左右端点时的最长的回文串长度,然后枚举割点取 max 即可。

当然也可以二分答案。

#include <bits/stdc++.h>

using std::cin;     using std::cout;
using std::string;  using std::vector;

const int N = 1e5 + 5;

int n;
char s[N], t[N << 1];
vector <int> l(N << 1), r(N << 1), R(N << 1);

int modify() {
    int len = 0;
    t[++len] = '$', t[++len] = '#';
    for (int i = 0; i < strlen(s); ++i) t[++len] = s[i], t[++len] = '#';
    t[++len] = '\0';
    return len;
}

void manacher() {
    int mx = 0, mid = 0;
    for (int i = 1; i <= n; ++i) {
        R[i] = (i < mx) ? std::min(R[(mid << 1) - i], mx - i + 1) : 1;
        while (i - R[i] > 0 && i + R[i] <= n && t[i + R[i]] == t[i - R[i]]) R[i]++;
        if (R[i] + i - 1 > mx) mx = R[i] + i - 1, mid = i;

        l[i - R[i] + 1] = std::max(l[i - R[i] + 1], R[i] - 1);
        r[i + R[i] - 1] = std::max(r[i + R[i] - 1], R[i] - 1);
    }
}

int main() {
    std::ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> s;
    n = modify();

    manacher();
    for (int i = 2; i <= n; i += 2) l[i] = std::max(l[i], l[i - 2] - 2);
    for (int i = n - 2; i > 0; i -= 2) r[i] = std::max(r[i], r[i + 2] - 2);

    int ans = 0;
    for (int i = 2; i <= n; i += 2)
        if (l[i] && r[i]) ans = std::max(ans, l[i] + r[i]);

    cout << ans;
    return 0;
}

「Luogu P1659」[国家集训队]拉拉队排练

Luogu

只需要求出奇数长度的最长回文串,按照长度 sort 一遍枚举即可,注意这里需要用到快速幂。

#include <bits/stdc++.h>

using std::cin;     using std::cout;
using std::string;  using std::vector;
using i64 = long long;

const int mod = 19930726;
const int N = 1e6 + 5;

int n;
i64 k, ans;
string s;
vector <int> r, d;

void manacher() {
    r = vector <int> (n);
    for (int i = 0, id = 0, rr = -1; i < n; ++i) {
        r[i] = (i < rr) ? std::min(r[(id << 1) - i], rr - i + 1) : 1;
        while (i - r[i] >= 0 && i + r[i] < n && s[i - r[i]] == s[i + r[i]]) ++r[i];
        if (i + r[i] - 1 > rr) id = i, rr = i + r[i] - 1;
    }
}

i64 qpow(i64 a, int b) {
    i64 ret = 1;
    while (b) {
        if (b & 1) ret = ret * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return ret;
}

void count() {
    int cnt = 0;
    i64 tot = 0;
    ans = 1;

    for (int len = r[0], p = 0; len > 0 && tot < k; len -= 2) {
        while (p >= 0 && r[p] == len) cnt++, p++;
        if (cnt > k - tot) cnt = k - tot;
        ans = ans * qpow(1ll * len, cnt) % mod;
        tot += cnt;
    }
}

int main() {
    std::ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> k;
    cin >> s;

    manacher();
    std::sort(r.begin(), r.end(), [](int a, int b) { return a > b; });
    for (int i = 0; i < n; ++i) r[i] = (r[i] << 1) - 1;
    count();

    cout << ans;
    return 0;
}

Comments