最长回文子串

问题定义

给定一个字符串 s,找出其中最长的回文子串。暴力解法枚举所有子串并逐一判断是否为回文,时间复杂度为 O(n³);即使枚举每个”中心”再向外扩展,最坏也是 O(n²)。马拉车算法(Manacher’s Algorithm)利用回文的对称性,把这个问题优化到了 O(n)

核心思想

回文的两种类型

回文是中心对称的,但中心有两种形态:

  • 奇数长度:如 "aba",中心是单个字符 b
  • 偶数长度:如 "abba",中心位于两个字符 b 之间。

两种形态处理逻辑不同,是写起来容易出错的根源。

预处理:统一奇偶长度

在原始字符串的每个字符之间插入特殊分隔符 #,并在首尾加上不同的哨兵字符(如 ^$)避免边界检查:

原始字符串 "babad" → 预处理后 "^#b#a#b#a#d#$"

这样处理后,原串的奇数回文变成以 # 为中心,偶数回文变成以原字符为中心——奇偶两种情况被统一了,后续只需要处理”以某个位置为中心扩展”这一种逻辑。

关键步骤

len[] 数组的含义

定义 len[i] 为预处理串中第 i 个字符为中心的最长回文半径(包含中心自身)。由于插入了分隔符,真实回文长度 = len[i] – 1。例如 len[i] = 4 对应的实际回文长度为 3。

维护对称性:center 与 right

centerright 记录当前已知的”最右回文子串”的中心和右边界。处理位置 i 时,如果 i < right,说明 i 在已知回文内部,可以借助它关于 center 的对称点 mirror = 2 * center - i 的信息快速初始化:

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

这一行是整个算法的精华——它保证了每个位置最多只被真正”扩展”一次,从而把复杂度压到线性。

中心扩展与更新

  1. i 向两侧扩展,直到字符不相等,得到最终的 len[i]
  2. 若扩展后的右边界 i + len[i] 超过了当前的 right,则更新 center = iright = i + len[i]

示例:以 “babad” 为例

预处理后:^ # b # a # b # a # d # $,逐位计算 len[]

i=1: 边界字符,len=0
i=2: 扩展成 b#b,len=3,更新 center=2, right=5
i=3: 无法扩展,len=0
i=4: 对称初始化 len=min(5-4, len[2])=1,扩展成 a#b#a,len=4,更新 center=4, right=8
i=5: 无法扩展,len=0
i=6: 对称初始化 len=min(8-6, len[4])=2,扩展成 b#a#b,len=4,i+len=10 未超过 right,不更新
i=7: 无法扩展,len=0

最大 len = 4,真实回文长度 = 4 − 1 = 3。起点换算:start = (center - len) / 2,取 i=4(4-4)/2 = 0,即原串的 "bab"(取 i=6 则得到 "aba",两者都是最长回文)。

代码实现(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 只会单调向右移动。
  • 空间复杂度 O(n):len 数组与预处理串各占 O(n)。

马拉车算法是”用预处理和对称性换时间复杂度”的经典例子,掌握它对理解其他字符串算法(如 Z 函数、扩展 KMP)也很有帮助。

滚动至顶部