Manacher(最长回文子串)CSP-S

目录

💡 核心思想

Manacher 算法在 O(n) 时间内求出字符串中以每个字符为中心的最长回文半径。核心技巧是在字符间插入分隔符(如 #),将奇数长度和偶数长度的回文统一处理。利用对称性:如果当前点在已发现的最右回文串内部,则可以直接复制对称点的回文半径,只需少量扩展即可。

Manacher 是字符串竞赛的必备武器。看到"最长回文子串"就写中心扩展 O(n²)?Manacher 直接 O(n) 秒杀。核心就三个变量:当前最右回文边界 r,对应的中心 mid,以及每个位置的回文半径 p[i]。理解了对称性的利用,代码非常短。

🎯 直觉理解

想象你在沙漠里探测地下水。你已经挖了一口井(一个回文串),深度为 p[mid]。现在要探测新位置 i,如果 i 在你已挖的井的范围内,那么 i 的对称点 j = 2*mid - i 已经被探测过了,你可以直接参考 j 的深度,只需要再往下挖一点点验证。

📝 算法流程

  1. 在字符间插入 #,如 "aba" → "#a#b#a#"
  2. 初始化 p[i] = 0,mid = 0,r = 0
  3. 遍历每个位置 i:
  4. 如果 i < r,则 p[i] = min(p[2*mid - i], r - i)
  5. 中心扩展:while s[i+p[i]+1] == s[i-p[i]-1], p[i]++
  6. 如果 i + p[i] > r,更新 mid = i, r = i + p[i]
  7. 答案为 max(p[i]) - 1(因为插入了 #)

$$p[i] = \min(p[2 \cdot mid - i],\ r - i) \quad \text{(当 } i < r \text{ 时)}$$

📊 复杂度分析

指标复杂度
时间$O(n)$(每个字符最多被比较一次)
空间$O(n)$

💻 参考实现(C++)

C++ (C++17)
#include 
using namespace std;

int manacher(const string& s) {
    int n = s.size();
    string t = "#";
    for (char c : s) { t += c; t += '#'; }
    
    int m = t.size();
    vector p(m, 0);
    int mid = 0, r = 0, maxLen = 0;
    
    for (int i = 0; i < m; i++) {
        if (i < r) p[i] = min(p[2 * mid - i], r - i);
        while (i - p[i] - 1 >= 0 && i + p[i] + 1 < m && 
               t[i - p[i] - 1] == t[i + p[i] + 1]) p[i]++;
        if (i + p[i] > r) { mid = i; r = i + p[i]; }
        maxLen = max(maxLen, p[i]);
    }
    return maxLen; // 回文半径(含 #),实际长度为 maxLen
}

string longestPalindrome(const string& s) {
    int n = s.size();
    string t = "#";
    for (char c : s) { t += c; t += '#'; }
    
    int m = t.size();
    vector p(m, 0);
    int mid = 0, r = 0, center = 0;
    
    for (int i = 0; i < m; i++) {
        if (i < r) p[i] = min(p[2 * mid - i], r - i);
        while (i - p[i] - 1 >= 0 && i + p[i] + 1 < m && 
               t[i - p[i] - 1] == t[i + p[i] + 1]) p[i]++;
        if (i + p[i] > r) { mid = i; r = i + p[i]; }
        if (p[i] > p[center]) center = i;
    }
    
    int start = (center - p[center]) / 2;
    return s.substr(start, p[center]);
}

int main() {
    string s = "babad";
    cout << manacher(s) << endl;      // 3 ("bab" 或 "aba")
    cout << longestPalindrome(s) << endl;
    return 0;
}

⚠️ 常见坑点

分隔符选择——不要用字符串中已有的字符

p[i] 的含义——包含分隔符的回文半径,实际长度要减 1

数组越界——中心扩展时要检查边界

忘记更新 mid 和 r——找到更右的回文时要更新

📚 相关题目

题目来源难度备注
P3805 最长回文子串洛谷CSP-SManacher 模板题
P1659 字符串变换洛谷CSP-SManacher + 贪心
P5446 字符串统计洛谷CSP-SManacher + 计数