最长回文子串

马拉车算法最长回文子串 O(n) 对称复用

问题定义

给定字符串 s,找其中最长的回文子串。暴力枚举所有子串并判断回文是 O(n³);枚举每个”中心”向外扩展,最坏 O(n²)。马拉车算法(Manacher)利用回文的对称性,把复杂度压到 O(n)。

核心思想

回文的两种中心

回文中心对称,但中心有两种形态:奇数长度(如 "aba",中心是单个字符)和偶数长度(如 "abba",中心在两个字符之间)。两种形态处理逻辑不同,是写起来容易出错的根源。

预处理:插入 # 统一奇偶

在字符间插入分隔符 #,首尾加哨兵 ^、$(与任何字符都不等,扩展时自动停,免去越界判断):

马拉车预处理:插入井号统一奇偶回文中心
图 1:插入 # 后,奇偶回文都只有一个中心,处理逻辑只剩一种
原始字符串 "babad" → 处理串 "^#b#a#b#a#d#$"

原串的奇数回文仍以原字符为中心(”bab” → “#b#a#b#”),偶数回文以 # 为中心(”abba” → “#a#b#b#a#”)。之后只需要写”以某位置为中心扩展”这一种逻辑。

len 数组

len[i] = 处理串中以 i 为中心的回文半径(含中心)。由于 # 的存在,原串回文长度恰好等于 len[i]:比如 i=4 处 len=3,对应原串 “bab”(长度 3);偶数回文 “abba” 对应 i=5 处 len=4。奇偶统一后一条公式适用。

中心与右边界:对称复用

维护当前已知”最右回文”的 center 和 right。处理 i 时若 i < right,则 i 落在已知回文内部,可以借它关于 center 的镜像点 mirror = 2*center - i 的结果初始化:

len[i] = min(right - i, len[mirror])

这行是整个算法的关键:镜像范围内的回文和已知回文对称,不用重新扩展;扩展只发生在右边界推进时,而 right 单调右移,所以每个位置至多被扩展一次。

示例走查(”babad”)

处理串 ^ # b # a # b # a # d # $(下标 0..12),逐位算 len[]:

马拉车对称复用:len数组镜像初始化与中心扩展
图 2:i=6 先取镜像 len[2]=1 初始化,再扩展两步到 len=3
i=1: 扩展立即失败,len=0
i=2: 扩展成 "#b#",len=1,更新 center=2, right=3
i=3: 无法扩展,len=0
i=4: 直接扩展:t[5]=t[3]='#'、t[6]=t[2]='b'、t[7]=t[1]='#'
     len=3,更新 center=4, right=7     (span 1..7 = "#b#a#b#" = "bab")
i=5: 镜像初始化 len=min(7-5, len[3]=0)=0,无法扩展
i=6: 镜像初始化 len=min(7-6, len[2]=1)=1
     再扩展:t[8]=t[4]='a'、t[9]=t[3]='#',len=3
     更新 center=6, right=9            (span 3..9 = "#a#b#a#" = "aba")
i=8: 镜像初始化 len=min(9-8, len[4]=3)=1,无法扩展
最大 len=3 → 真实回文长度 3
start = (center - len) / 2:center=4 得 0 → "bab";center=6 得 1 → "aba"

注意 i=6 的两次”免费扩展”来自镜像,这是暴力中心扩展做不到的。

代码实现(C++)

#include <string>
#include <vector>
#include <algorithm>
using namespace std;

string longestPalindrome(string s) {
    if (s.empty()) return "";
    // 预处理:插入 # 并在首尾加哨兵
    string t = "^";
    for (char c : s) { t += '#'; t += c; }
    t += "#$";

    int n = t.size();
    vector<int> len(n, 0);
    int center = 0, right = 0;
    int max_len = 0, max_center = 0;

    for (int i = 1; i < n - 1; i++) {
        int mirror = 2 * center - i;
        if (i < right)
            len[i] = min(right - i, len[mirror]);

        // 中心扩展(哨兵保证不会越界)
        while (t[i + len[i] + 1] == t[i - len[i] - 1])
            len[i]++;

        if (i + len[i] > right) {
            center = i;
            right = i + len[i];
        }
        if (len[i] > max_len) {
            max_len = len[i];
            max_center = i;
        }
    }
    int start = (max_center - max_len) / 2;
    return s.substr(start, max_len);
}

复杂度与总结

  • 时间 O(n):镜像初始化直接复用结果,真正的扩展只发生在 right 推进时,而 right 单调右移——每个字符至多被扩展一次;
  • 空间 O(n):len 数组与处理串各占 O(n);
  • 马拉车是”用预处理和对称性换时间”的典型,掌握后对理解 Z 函数、扩展 KMP 也有帮助。
滚动至顶部