Manacher:线性时间最长回文子串
Manacher 把奇偶回文统一成一种形式,并用“镜像半径”跳过已经证明相等的部分,只在右边界外继续扩展。
一句话算法
Manacher 把奇偶回文统一成一种形式,并用“镜像半径”跳过已经证明相等的部分,只在右边界外继续扩展。
问题模型
给定一个长度为
回文串指正着读和反着读完全相同的字符串,例如:
aba
abba
anana
朴素做法枚举每个中心并向两侧扩展。中心有
Manacher 将这个问题优化到
核心直觉
回文串有对称性。若我们已经知道某个中心 center 的回文区间最右端为 right,那么区间内部的新位置 i,可以先看它关于 center 的镜像点:
mirror = 2 * center - i
镜像点已经算过回文半径,所以 i 至少可以继承一部分半径,不需要从 0 开始重新比较。
另一个问题是奇偶回文分裂:
aba -> 奇数长度回文
abba -> 偶数长度回文
Manacher 在字符之间插入 #,把它们都变成奇数长度回文:
aba -> #a#b#a#
abba -> #a#b#b#a#
这样代码只需要处理一种中心。
算法步骤
- 预处理字符串:
- 在字符之间插入
#; - 在两端加入不同哨兵,防止扩展越界。
- 在字符之间插入
- 维护数组
p[i],表示处理后字符串中以i为中心的回文半径。 - 维护当前最右回文区间:
center:该区间中心;right:该区间右边界。
- 枚举每个中心
i:- 若
i < right,用镜像点mirror = 2 * center - i初始化p[i]; - 再从当前半径开始继续向两边扩展;
- 若
i + p[i]超过right,更新center和right。
- 若
- 在所有
p[i]中取最大值,映射回原字符串位置。
算法证明
关键不变量:枚举到位置 i 时,right 是当前已经处理过的回文中最靠右的边界。
若 i < right,说明 i 落在当前最右回文区间内部。由于回文区间关于 center 对称,i 的镜像点 mirror 已经拥有一段可复用的回文半径。
有两种情况:
p[mirror]完全落在当前最右回文区间内,那么p[i] = p[mirror]直接成立。p[mirror]超出当前最右回文区间,那么i至少能继承到right - i,超出的部分必须重新比较。
因此初始化:
p[i] = min(p[mirror], right - i)
是安全的,不会漏掉答案。随后继续暴力扩展,直到不能扩展为止,所以每个中心的最终半径正确。
时间复杂度的直觉是:right 只会向右移动。每次成功扩展都会推动 right,失败扩展每个中心最多发生一次,所以总比较次数是线性的。
复杂度分析
设原字符串长度为
- 预处理后字符串长度为
。 - Manacher 主循环时间复杂度:
。 - 空间复杂度:
。
代码实现
模板输入一个不含空格的字符串,输出最长回文子串长度和这个子串。
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.