Manacher:线性时间最长回文子串

1 一句话算法

Manacher 把奇偶回文统一成一种形式,并用“镜像半径”跳过已经证明相等的部分,只在右边界外继续扩展。

2 问题模型

给定一个长度为 nn 的字符串 ss,求最长回文子串。

回文串指正着读和反着读完全相同的字符串,例如:

aba
abba
anana

朴素做法枚举每个中心并向两侧扩展。中心有 O(n)O(n) 个,每次扩展最坏 O(n)O(n),总复杂度 O(n2)O(n^2)。

Manacher 将这个问题优化到 O(n)O(n)。

3 核心直觉

回文串有对称性。若我们已经知道某个中心 center 的回文区间最右端为 right,那么区间内部的新位置 i,可以先看它关于 center 的镜像点:

mirror = 2 * center - i

镜像点已经算过回文半径,所以 i 至少可以继承一部分半径,不需要从 0 开始重新比较。

另一个问题是奇偶回文分裂:

aba   -> 奇数长度回文
abba  -> 偶数长度回文

Manacher 在字符之间插入 #,把它们都变成奇数长度回文:

aba  -> #a#b#a#
abba -> #a#b#b#a#

这样代码只需要处理一种中心。

4 算法步骤

  1. 预处理字符串:
    • 在字符之间插入 #;
    • 在两端加入不同哨兵,防止扩展越界。
  2. 维护数组 p[i],表示从中心到右侧第一个不匹配位置的距离,初始为 11。已知回文闭区间为 [i-p[i]+1, i+p[i]-1],对应原串长度为 p[i]-1。
  3. 维护当前最右回文区间:
    • center:该区间中心;
    • right:该区间右侧的第一个位置(不包含在回文内),即 center + p[center]。
  4. 枚举每个中心 i:
    • 若 i < right,用镜像点 mirror = 2 * center - i 初始化 p[i];
    • 再从当前半径开始继续向两边扩展;
    • 若 i + p[i] 超过 right,更新 center 和 right。
  5. 在所有 p[i] 中取最大值,映射回原字符串位置。

5 算法证明

关键不变量:枚举到位置 i 时,right 是当前已经处理过的回文中最靠右的边界。

若 i < right,说明 i 落在当前最右回文区间内部。由于回文区间关于 center 对称,i 的镜像点 mirror 已经拥有一段可复用的回文半径。

有两种情况:

  1. 若 p[mirror] < right-i,镜像回文严格落在已知区间内部,它的下一对字符已经失配,对称后同样失配,所以 p[i] = p[mirror]。
  2. 若 p[mirror] >= right-i,只能先确认到区间边界,即初始化为 right-i;能否继续,需要比较边界外的字符。

因此初始化:

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

是安全的,不会漏掉答案。随后继续暴力扩展,直到不能扩展为止,所以每个中心的最终半径正确。

时间复杂度的直觉是:right 只会向右移动。每次成功扩展都会推动 right,失败扩展每个中心最多发生一次,所以总比较次数是线性的。

6 复杂度分析

设原字符串长度为 nn。

  • 预处理后字符串长度为 O(n)O(n)。
  • Manacher 主循环时间复杂度:O(n)O(n)。
  • 空间复杂度:O(n)O(n)。
注意哨兵与长度限制

C++ 定长数组版要求原串长度不超过 MAXN = 110000,并且输入不包含两端哨兵 & 和 ^;Python 版使用相同哨兵限制。分隔符 # 按字符位置的奇偶性隔开原字符,即使原串含有 # 也不影响比较;哨兵则必须不在输入中。

本文两个程序都读入一个不含空白的字符串;处理任意字符集时,可把原字符编码为非负整数,使用不同的负数作为分隔符和哨兵。

7 代码实现

模板输入一个不含空格的字符串,输出最长回文子串长度和这个子串。

#include <bits/stdc++.h>
using namespace std;

constexpr int MAXN = 110000;            // 原始字符串最大长度

// Manacher 模板(不使用 std::string)
struct Manacher {
    // 变换后长度上限:2*MAXN + 少量哨兵
    static const int SZ = MAXN * 2 + 5;
    char t[SZ];    // 变换后的字符串:^ # a # b # ... # $
    int p[SZ];     // 半径数组(在 t 上的扩展长度)
    int m = 0;     // t 的实际长度(含终止符)

    // 构造变换串并计算 p[]
    // 输入:C 风格字符串 s(以 '\0' 结尾)
    void build(const char *s) {
        int n = (int)strlen(s);
        int k = 0;
        t[k++] = '&';   // 左哨兵,防止越界
        t[k++] = '#';
        for (int i = 0; i < n; ++i) {
            t[k++] = s[i];
            t[k++] = '#';
        }
        t[k++] = '^';   // 右哨兵
        t[k] = '\0';
        m = k;

        // 初始化半径数组
        memset(p,0,sizeof(p));

        // Manacher 主循环
        int center = 0, right = 0;
        for (int i = 1; i < m - 1; ++i) {
            int mirror = 2 * center - i;
            if (i < right) p[i] = min(right - i, p[mirror]);
            else p[i] = 1; // 长度至少是1

            // 暴力扩展:安全因为有哨兵
            while (t[i + p[i]] == t[i - p[i]]) ++p[i];

            // 更新中心与右边界
            if (i + p[i] > right) {
                center = i;
                right = i + p[i];
            }
        }
    }

    // 获取最长回文子串,返回长度;通过引用参数 l,r 返回原串上的左闭右闭索引(0-based)
    int longest(int &l, int &r) const {
        int best_len = 0, best_center = 0;
        for (int i = 1; i < m - 1; ++i) {
            if (p[i]-1 > best_len) {
                best_len = p[i]-1;
                best_center = i;
            }
        }
        if (best_len == 0) { l = 0; r = -1; return 0; }
        // 将 t 中的位置映射回原串的索引
        l = (best_center - best_len) / 2;
        r = l + best_len - 1;
        return best_len;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    static char s[MAXN + 5];
    if (scanf("%110000s", s) != 1) return 0;   // 读取一个单词(竞赛常用)
    // 若需读取整行(含空格),使用 fgets 或 fgets + 去除末尾换行

    Manacher man;
    man.build(s);

    int L, R;
    int len = man.longest(L, R);
    printf("%d\n", len);
    if (len > 0) {
        // 输出子串:注意 printf 的 %.*s 用法
        printf("%.*s\n", len, s + L);
    }
    return 0;
}
# Manacher:线性时间求最长回文子串。
# build(s) 后 longest() 返回 (长度, l, r),[l, r] 是原串上的 0-based 闭区间;
# 无回文(空串)时返回 (0, 0, -1)。C++ 用 int& 出参,Python 改为元组返回。
# 变换串为 & # c1 # c2 # ... # ^,两端哨兵互不相等,扩展时无需判边界。
# C++ 用定长数组 MAXN=110000,Python list 按 len(s) 动态分配,没有长度上限。

IntSeq = list[int]  # p:半径数组,p[i] 为变换串上以 i 为中心的扩展长度
Text = list[str]  # t:变换后的字符序列,首尾各有一个哨兵


class Manacher:
    """t / p / m 与 C++ 成员一一对应;m 是含哨兵的变换串长度。"""

    t: Text
    p: IntSeq
    m: int

    def __init__(self) -> None:
        self.t = []
        self.p = []
        self.m = 0

    def build(self, s: str) -> None:
        """构造变换串并计算半径数组;可对同一对象反复 build 换新串。"""
        # 交错插入 '#' 后,任意回文长度都是奇数,一个数组即可同时处理奇偶回文。
        t: Text = ["&", "#"]
        for ch in s:
            t.append(ch)
            t.append("#")
        t.append("^")
        self.t = t
        self.m = len(t)
        self.p = [0] * self.m

        center = 0  # 当前已知最靠右回文的中心
        right = 0  # 该回文的右边界(不含)
        for i in range(1, self.m - 1):  # 跳过两个哨兵
            mirror = 2 * center - i  # i 关于 center 的对称点
            if i < right:
                # 对称点的半径受右边界限制,取两者较小值作为初值。
                self.p[i] = min(right - i, self.p[mirror])
            else:
                self.p[i] = 1  # 至少包含 i 自身
            # 暴力扩展;因为哨兵互不相等,最右/最左一定停得住。
            while self.t[i + self.p[i]] == self.t[i - self.p[i]]:
                self.p[i] += 1
            if i + self.p[i] > right:
                center = i
                right = i + self.p[i]

    def longest(self) -> tuple[int, int, int]:
        """返回 (最长回文长度, l, r);无解返回 (0, 0, -1)。"""
        best_len = 0
        best_center = 0
        for i in range(1, self.m - 1):
            if self.p[i] - 1 > best_len:  # p[i]-1 正是该中心的原串回文长度
                best_len = self.p[i] - 1
                best_center = i
        if best_len == 0:
            return 0, 0, -1
        # 把变换串位置映射回原串下标:中心左侧有 (p[i]-1) 个原字符,间隔是 '#'。
        l = (best_center - best_len) // 2
        r = l + best_len - 1
        return best_len, l, r


if __name__ == "__main__":
    import sys
    data = sys.stdin.read().split()
    if data:
        s = data[0]
        solver = Manacher()
        solver.build(s)
        length, left, right = solver.longest()
        print(length)
        if length:
            print(s[left:right + 1])

8 测试用例

输入:

banana

输出:

5
anana

解释:banana 的最长回文子串是 anana,长度为 5。

9 应用分类详解

Manacher 的本质是“快速得到每个中心的最大回文半径”。只要题目围绕回文子串展开,并且需要大量中心扩展,就应该想到它。

一、 最长回文子串

典型模式: 给一个字符串,求最长连续回文子串。

识别信号: 题目明确出现“最长回文子串”。

核心建模: 用 Manacher 计算所有中心半径,再取最大值。

应用场景 经典题目 核心思路
最长回文子串模板 luogu P3805 直接取最大回文半径
普通文本回文检测 LeetCode 5 返回最长回文子串

二、 回文半径参与统计

典型模式: 不只要求最长,还要统计满足条件的回文数量或贡献。

识别信号: 出现“所有回文子串”“长度限制”“贡献和”。

核心建模: 每个中心的半径代表以它为中心的多个回文,按半径累加贡献。

应用场景 经典题目 核心思路
回文数量统计 回文计数类题 每个中心贡献由半径决定
固定长度回文判断 查询类题 预处理半径后判断区间是否被某中心覆盖

三、 与其他算法组合

典型模式: 回文只是题目中的一个约束,还需要 DP、贪心或数据结构。

识别信号: 出现“把字符串切成若干回文”“两个回文拼接”“回文前后缀”。

核心建模: 先用 Manacher 得到回文边界,再交给 DP 或前后缀统计。

应用场景 经典题目 核心思路
双回文拼接 luogu P4555 先求每个位置左右可达的最长回文
回文长度选择 luogu P1659 用半径得到可选回文长度

10 经典例题

1. luogu P3805

Manacher 模板题。目标是求最长回文子串长度。

2. luogu P4555

要求两个回文串拼接的最大长度。Manacher 提供每个位置附近的回文半径,再做左右信息合并。

3. luogu P1659

需要从回文长度中选择若干项。Manacher 先枚举所有中心的回文半径,再按长度统计。

11 参考

  • Manacher, G. A linear-time algorithm for finding maximal palindromes in strings.

12 补充推导

半径为什么减一

12.1 从变换串长度还原原串长度

本模板的 p[i] 包含中心本身,因此变换串中的回文长度为 2p[i]−12p[i]-1。

设对应的原串回文长度为 LL。在每个原字符两侧插入分隔符后,包含 LL 个原字符和 L+1L+1 个分隔符,总长为 2L+12L+1。最大回文的两端都是分隔符:若两端都是原字符,它们外面的两个分隔符仍相等,还能扩展一步。

于是:

2p[i]−1=2L+1⟹L=p[i]−1. 2p[i]-1=2L+1 \quad\Longrightarrow\quad L=p[i]-1.

这同时覆盖奇数与偶数长度回文。若中心是一个分隔符且两侧不匹配,便有 p[i] = 1、L=0L=0。

本实现左哨兵位于下标 00,原字符依次位于变换串的 2,4,6,…2,4,6,\ldots 位置。对于原串长度 L=p[i]−1L=p[i]-1 的回文,原串起点为 l = (i-L)/2(整数除法),终点为 r = l+L-1。

线性时间的计数证明

12.2 把比较分成成功与失败两类

镜像回文严格落在已知最右回文内部时,半径已经确定,下一次比较立即失败。其余中心从已知边界开始扩展,每次成功比较都会使可达的最右端向右推进。

right 只增不减,最多前进 O(n)O(n) 次,所以成功扩展总数为 O(n)O(n)。每个中心的扩展循环至多以一次失败比较结束;中心只有 O(n)O(n) 个,所以失败比较总数也是 O(n)O(n)。

两类相加,主循环总时间为 O(n)O(n)。镜像继承的半径不需要重新逐字符验证。

13 相关题解

暂无相关题解