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

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

一句话算法

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

问题模型

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

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

aba
abba
anana

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

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

核心直觉

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

mirror = 2 * center - i

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

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

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

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

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

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

算法步骤

  1. 预处理字符串:
    • 在字符之间插入 #
    • 在两端加入不同哨兵,防止扩展越界。
  2. 维护数组 p[i],表示处理后字符串中以 i 为中心的回文半径。
  3. 维护当前最右回文区间:
    • center:该区间中心;
    • right:该区间右边界。
  4. 枚举每个中心 i
    • i < right,用镜像点 mirror = 2 * center - i 初始化 p[i]
    • 再从当前半径开始继续向两边扩展;
    • i + p[i] 超过 right,更新 centerright
  5. 在所有 p[i] 中取最大值,映射回原字符串位置。

算法证明

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

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

有两种情况:

  1. p[mirror] 完全落在当前最右回文区间内,那么 p[i] = p[mirror] 直接成立。
  2. p[mirror] 超出当前最右回文区间,那么 i 至少能继承到 right - i,超出的部分必须重新比较。

因此初始化:

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

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

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

复杂度分析

设原字符串长度为 nn

  • 预处理后字符串长度为 O(n)O(n)
  • Manacher 主循环时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

代码实现

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

cpp
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#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("%s", 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; }

测试用例

输入:

banana

输出:

5
anana

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

应用分类详解

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

一、最长回文子串

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

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

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

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

二、回文半径参与统计

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

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

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

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

三、与其他算法组合

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

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

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

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

经典例题

1. luogu-P3805

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

2. luogu-P4555

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

3. luogu-P1659

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

参考

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