Bootstrap

Manacher(马拉车)算法详解,原理分析

零、问题引入

给定一个长度为n的字符串 s,请找到所有对 (i, j) 使得子串 s[i, …, j] 为一个回文串。

考虑枚举中点向外扩展:O(N^2)

考虑区间DP:O(N^2),还不如直接中心扩展(

考虑字符串哈希,似乎可以枚举左端点二分右端点O(NlogN)解决,但是字符串哈希大多数时候不会是正解(

考虑后缀数组,……板子不会背

Glenn K. Manacher 在 1975 年提出 一种在线性时间内求解该问题的算法。

一、Manacher 算法

Manacher 算法 可以在 O(n)的时间内求出一个字符串中的最长回文串,Manacher 算法其实是对中心扩展法的改进。

1.1 算法原理

1.1.1 改造字符串

在字符之间和串两端插入不在题目字符集内的任意英文字符,改造后,所有的偶回文串都会变成奇回文串,方便统一处理,避免像中心扩展法一样还要处理奇偶。

下面用 ‘#’ 代表插入字符

奇回文串:aba	改造后:#a#b#a#
偶回文串:abba	改造后:#a#b#b#a#
1.1.2 回文半径

d[i] 代表以 i 为中心的最长回文串的长度的一半,称为回文半径。

回文半径d[i]满足 d[i] - 1 为原字符串该位置为中心的最长回文串长度

如:

i:	1 2 3 4 5 6 7
s:	# a # b # a #
d:	1 2 1 4 1 2 1

d[4] - 1 = 3,代表 "aba"的长度

d[2] - 1 = 1,代表"a"的长度

1.1.3 加速盒子

算法过程中我们维护右端点最靠右的最长回文串 s[l, r],利用盒子[l, r],借助之前的状态来加速计算新的状态。盒内 d[i] 可以利用对称点的 d值转移,盒外暴力。

i:	1 2   |   3 4 5 6 7 8 9   |
s:	# a   |   # a # b # a #   |
d:	1 2   |   3 2 1 4 1 2 1   |

如上面这个例子,我们已经计算出了 d[6] = 4,如何计算d[7]呢?

s[7]盒子[3, 9]这个回文串中,它对称位置为s[5],根据回文半径的定义,d[7] 和 d[5] 是由公共部分的,为 min(9 - 7, d[5]),即不能超过盒子,也不能大于d[5],至于有没有超过盒子或者大于d[5],我们暴力向外扩展检测可以判断。

1.2 算法流程

  • 改造长度为n的字符串为2n + 1的字符串
  • 遍历字符串计算d[],假设当前遍历到下标 i ,最靠右回文串(加速盒子为)[l, r]
    • 如果 i <= r,即 i 在盒内,更新d[i] = min(r - i + 1, d[l + r - i])
  • 暴力向外扩展d[i]
  • 如果i + d[i] - 1 > r,则更新盒子为 l = i - d[i] + 1, r = i + d[i] - 1

如果我们根据对称点更新完d,没有到达盒子边界,那么暴力扩展第一次就会失败

否则我们会向右移动r,而r最多移动到字符串末尾,相当于滑动窗口滑动,是O(N)的

总体时间复杂度自然是O(N)的

1.3 代码实现

具体实现中,个人喜欢记录最右回文串的中心下标 j ,因为一直d[j],所以和[l, r]效果等价

std::vector<int> manacher(const std::string& s) {
    std::vector<int> t{0};
    for (char c : s)
        t.push_back(c), t.push_back(0);
    int n = t.size();
    std::vector<int> r(n);
    for (int i = 0, j = 0; i < n; ++ i) {
        if (j + r[j] > i)
            r[i] = std::min(r[2 * j - i], j + r[j] - i);
        while (i - r[i] >= 0 && i + r[i] < n && t[i - r[i]] == t[i + r[i]])
            ++ r[i];
        if (i + r[i] > j + r[j])
            j = i;
    }
    return r;
}

二、OJ练习

2.1 模板

原题链接

P3805 【模板】manacher - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

板子题直接交就行

AC代码

#include <bits/stdc++.h>
#define sc scanf
using i64 = long long;
using PII = std::pair<int, int>;
constexpr int inf32 = 1e9 + 7;
constexpr i64 inf64 = 1e18 + 7;

std::vector<int> manacher(std::string s) {
    std::vector<int> t{0};
    for (char c : s)
        t.push_back(c), t.push_back(0);
    int n = t.size();
    std::vector<int> r(n);
    for (int i = 0, j = 0; i < n; ++ i) {
        if (j + r[j] > i)
            r[i] = std::min(r[2 * j - i], j + r[j] - i);
        while (i - r[i] >= 0 && i + r[i] < n && t[i - r[i]] == t[i + r[i]])
            ++ r[i];
        if (i + r[i] > j + r[j])
            j = i;
    }
    return r;
}

void solve() {
    std::string s;
    std::cin >> s;
    std::vector<int> r = manacher(s);
    std::cout << *std::max_element(r.begin(), r.end()) - 1;
}

int main() {
#ifdef DEBUG
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
#endif
    std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
    int _ = 1;
    // std::cin >> _;
    while (_ --)
        solve();
    return 0;
}

2.2 回文子串

原题链接

647. 回文子串

思路分析

每个位置为中心长度为x的回文串的贡献是 (x + 1) / 2,我们跑马拉车,然后计算即可

AC代码

std::vector<int> Manacher(const std::string& s) {
    std::vector<int> t{0};
    for (char c : s)
        t.push_back(c), t.push_back(0);
    int n = t.size();
    std::vector<int> r(n);
    for (int i = 0, j = 0; i < n; ++ i) {
        if (i < j + r[j]) 
            r[i] = std::min(j + r[j] - i, r[2 * j - i]);
        while (i - r[i] >= 0 && i + r[i] < n && t[i - r[i]] == t[i + r[i]])
            ++ r[i];
        if (i + r[i] > j + r[j]) j = i;
    }
    return r;
}
class Solution {
public:
    int countSubstrings(string s) {
        auto r = Manacher(s);
        int res = 0;
        for (int x : r)
            res += x / 2;
        return res;
    }
};
;