- admin 的博客
Manacher 算法 (马拉车算法)
- @ 2026-6-7 20:02:57
一、算法概述
Manacher 算法是一种在 线性时间 O(n) 内求解字符串中最长回文子串的高效算法。由 Glenn Manacher 于 1975 年提出,相比朴素的中心扩展法 O(n²) 和动态规划法 O(n²),它将时间复杂度降到了最优的 O(n)。
二、核心思想
Manacher 算法的核心思想包括两个关键技巧:
-
插入分隔符,统一处理奇偶回文
- 在字符串每个字符之间(包括首尾)插入一个特殊分隔符(通常用
#),使得所有回文串的长度都变为奇数,从而可以用统一的中心扩展方式处理。 - 例如:
"aba"→"#a#b#a#","aa"→"#a#a#"。
- 在字符串每个字符之间(包括首尾)插入一个特殊分隔符(通常用
-
利用已知回文信息避免重复计算
- 维护一个回文半径数组
p[i],表示以位置i为中心的最长回文半径。 - 维护当前最右回文边界
R及其对应的中心C。当计算p[i]时,利用i关于C的对称点mirror = 2*C - i的已知信息,进行加速。
- 维护一个回文半径数组
三、算法步骤 / 伪代码
输入:原始字符串 s
输出:最长回文子串
1. 预处理:在 s 的每个字符间插入 '#',并在首尾加哨兵 '^' 和 '$',得到字符串 t
2. 初始化数组 p[0..n-1] = 0,回文中心 C = 0,最右边界 R = 0
3. 遍历 i = 1 到 n-1:
a. 计算镜像位置 mirror = 2*C - i
b. 如果 i < R,则 p[i] = min(R - i, p[mirror])
c. 以 i 为中心向两边扩展,更新 p[i]
d. 如果 i + p[i] > R,则更新 C = i,R = i + p[i]
4. 找到 p[i] 的最大值及其中心 maxCenter
5. 从原始字符串 s 中提取最长回文子串
四、C++ 代码实现
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Manacher {
public:
// 求最长回文子串,返回 (长度, 子串)
pair<int, string> longestPalindrome(const string& s) {
if (s.empty()) return {0, ""};
// 1. 预处理:插入分隔符
string t = preprocess(s);
int n = t.size();
vector<int> p(n, 0); // 回文半径数组
// 2. Manacher 核心
int C = 0; // 当前最右回文串的中心
int R = 0; // 当前最右回文串的右边界
for (int i = 1; i < n - 1; i++) {
// 利用镜像初始化 p[i]
int mirror = 2 * C - i;
if (i < R) {
p[i] = min(R - i, p[mirror]);
}
// 中心扩展
while (t[i + p[i] + 1] == t[i - p[i] - 1]) {
p[i]++;
}
// 更新最右边界
if (i + p[i] > R) {
C = i;
R = i + p[i];
}
}
// 3. 找到最大回文半径及其中心
int maxLen = 0;
int centerIdx = 0;
for (int i = 1; i < n - 1; i++) {
if (p[i] > maxLen) {
maxLen = p[i];
centerIdx = i;
}
}
// 4. 从原字符串中提取最长回文子串
// 原始字符串中的起始位置 = (centerIdx - maxLen) / 2
int start = (centerIdx - maxLen) / 2;
string result = s.substr(start, maxLen);
return {maxLen, result};
}
private:
// 预处理:插入分隔符和哨兵
// 原始: "aba" → "^#a#b#a#$"
string preprocess(const string& s) {
string t = "^";
for (char c : s) {
t += "#";
t += c;
}
t += "#$";
return t;
}
};
// ==================== 测试用例 ====================
int main() {
Manacher m;
// 测试1: 奇数长度回文
auto [len1, str1] = m.longestPalindrome("babad");
cout << "输入: babad" << endl;
cout << "最长回文子串: " << str1 << " (长度: " << len1 << ")" << endl;
cout << endl;
// 测试2: 偶数长度回文
auto [len2, str2] = m.longestPalindrome("cbbd");
cout << "输入: cbbd" << endl;
cout << "最长回文子串: " << str2 << " (长度: " << len2 << ")" << endl;
cout << endl;
// 测试3: LeetCode 5 示例
auto [len3, str3] = m.longestPalindrome("abbcacacab");
cout << "输入: abbcacacab" << endl;
cout << "最长回文子串: " << str3 << " (长度: " << len3 << ")" << endl;
cout << endl;
// 测试4: 全部相同字符
auto [len4, str4] = m.longestPalindrome("aaaaaa");
cout << "输入: aaaaaa" << endl;
cout << "最长回文子串: " << str4 << " (长度: " << len4 << ")" << endl;
cout << endl;
// 测试5: 单字符
auto [len5, str5] = m.longestPalindrome("a");
cout << "输入: a" << endl;
cout << "最长回文子串: " << str5 << " (长度: " << len5 << ")" << endl;
cout << endl;
// 测试6: 空字符串
auto [len6, str6] = m.longestPalindrome("");
cout << "输入: (空)" << endl;
cout << "最长回文子串: " << str6 << " (长度: " << len6 << ")" << endl;
return 0;
}
五、复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(n) — 虽然有两层循环,但内层 while 每次成功扩展都会使 R 增大,而 R 最多从 0 增长到 n,所以总扩展次数为 O(n)。每个字符最多被访问常数次。 |
| 空间复杂度 | O(n) — 需要存储预处理后的字符串 t(长度约 2n+3)和回文半径数组 p(长度约 2n+3)。 |
六、应用场景
- 最长回文子串:字符串处理中最经典的问题(LeetCode 5)。
- 回文子串计数:统计一个字符串中有多少个回文子串(LeetCode 647)。
- 分割回文串:判断能否将字符串分割成若干回文子串。
- DNA 序列中的回文结构:生物信息学中寻找回文序列。
- 文本编辑器:检测对称文本模式。
- 竞赛编程:几乎所有涉及回文串的高效问题。
七、经典例题
LeetCode 5. 最长回文子串
题目描述:给定一个字符串 s,找到 s 中最长的回文子串。
示例:
- 输入:
"babad",输出:"bab"("aba"也是有效答案) - 输入:
"cbbd",输出:"bb"
解法说明:使用上述 Manacher 算法,O(n) 时间内即可解决。预处理后遍历一遍,维护回文半径数组,最后根据最大半径和中心点位置还原最长回文子串。上述 C++ 代码可直接解答此题。
说明:以上 C++ 代码完整可编译,使用
g++ -std=c++17 -O2 filename.cpp -o main编译后直接运行即可看到所有测试用例的输出。