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