Manacher 算法:在线性时间内寻找最长回文子串
Manacher 算法(马拉车算法)是一种用于在线性时间
O(n)内找到字符串中最长回文子串的高效算法。它巧妙地利用了回文串的对称性,避免了暴力解法O(n²)的时间复杂度。
📚 1. 问题背景
问题:给定一个字符串 s,找出其中最长的回文子串。
回文串:正读和反读都相同的字符串,例如 "aba"、"abba"。
暴力解法:
- 枚举每个可能的中心点。
- 从中心向两边扩展,检查是否为回文。
- 时间复杂度:
O(n²)。
Manacher 算法:通过预处理和巧妙利用回文对称性,将时间复杂度优化到 O(n)。
🧩 2. 核心思想与预处理
2.1 预处理:统一奇偶回文
Manacher 算法的一个关键步骤是预处理字符串,以统一处理奇数长度和偶数长度的回文串。
- 问题:奇数回文(如
"aba")有明确的中心字符,而偶数回文(如"abba")的中心在两个字符之间。 - 解决:在原字符串的每个字符之间以及首尾插入一个不会在原字符串中出现的分隔符(通常用
'#'),并在首尾也加上分隔符。
示例:
原字符串: a b a a b a
预处理后: # a # b # a # a # b # a #
效果:
- 所有回文串的长度都变成了奇数。
- 原来的奇数回文
"aba"变成#a#b#a#,中心是'b'。 - 原来的偶数回文
"abba"变成#a#b#b#a#,中心是中间的'#'。 - 这样,我们只需要处理一种情况(奇数长度回文),大大简化了逻辑。
2.2 定义 P 数组
P[i]表示在预处理后的字符串中,以位置i为中心的最长回文半径。- 半径:从中心
i到回文串边界的长度(包含中心)。 - 回文长度:
P[i]就是该回文串的长度(因为预处理后都是奇数长度)。
示例:
字符串: # a # b # a # a # b # a #
索引 i: 0 1 2 3 4 5 6 7 8 9 10 11 12
P[i] 值: 1 2 1 2 1 4 1 4 1 2 1 2 1
i=5时,P[5]=4,表示以s[5]='a'为中心的最长回文是#a#a#a#,半径为 4,长度为 4。- 注意:这个回文对应原字符串中的
"aba"。
🧠 3. 核心算法:利用回文对称性
Manacher 算法的精髓在于,它利用了已知回文串的对称性来加速计算。
3.1 关键变量
center:当前已知的、能覆盖最右端的回文串的中心。right:当前已知的、能覆盖的最右边界(right = center + P[center])。
3.2 算法流程
遍历预处理后的字符串的每个位置 i:
-
初始化
P[i]:- 如果
i < right,说明i在center的回文覆盖范围内。 - 找到
i关于center的对称点j = 2 * center - i。 - 利用对称性,
P[i]的初始值可以设为min(P[j], right - i)。P[j]:对称点j的回文半径。right - i:i到右边界right的距离。- 取最小值是因为
i的回文不能超过center回文的右边界。
- 如果
i >= right,说明i不在任何已知回文的覆盖范围内,P[i]初始化为 1(至少是自身)。
- 如果
-
尝试扩展:
- 在
P[i]的初始值基础上,尝试从i - P[i]和i + P[i]开始,继续向两边扩展,只要字符相等就增加P[i]。
- 在
-
更新
center和right:- 如果
i + P[i] > right,说明以i为中心的回文扩展到了比之前更远的右边界。 - 更新
center = i和right = i + P[i]。
- 如果
🧪 4. 详细示例
以字符串 "abaab" 为例:
-
预处理:
s = "#a#b#a#a#b#"(为简化,我们只考虑到#a#b#a#a#b#) -
遍历过程:
i | 字符 | center | right | j (对称点) | P[i] 初始值 | 扩展后 P[i] | 是否更新 center/right |
|---|---|---|---|---|---|---|---|
| 0 | # | 0 | 1 | - | 1 | 1 | 是 (center=0, right=1) |
| 1 | a | 0 | 1 | -1 | 1 | 2 | 是 (center=1, right=3) |
| 2 | # | 1 | 3 | 0 | min(P[0]=1, 3-2=1)=1 | 1 | 否 |
| 3 | b | 1 | 3 | -1 | 1 | 2 | 是 (center=3, right=5) |
| 4 | # | 3 | 5 | 2 | min(P[2]=1, 5-4=1)=1 | 1 | 否 |
| 5 | a | 3 | 5 | 1 | min(P[1]=2, 5-5=0)=0 -> 1 | 4 | 是 (center=5, right=9) |
| 6 | # | 5 | 9 | 4 | min(P[4]=1, 9-6=3)=1 | 1 | 否 |
| 7 | a | 5 | 9 | 3 | min(P[3]=2, 9-7=2)=2 | 2 | 否 |
| 8 | # | 5 | 9 | 2 | min(P[2]=1, 9-8=1)=1 | 1 | 否 |
| 9 | b | 5 | 9 | 1 | min(P[1]=2, 9-9=0)=0 -> 1 | 2 | 否 |
| 10 | # | 5 | 9 | 0 | min(P[0]=1, 9-10<0)=1 | 1 | 否 |
- 结果:
P[5] = 4是最大值。- 回文长度为 4,对应原字符串中的
"abba"(注意:P[i]的值就是回文长度)。 - 原字符串中的起始位置可以通过
(i - P[i]) / 2计算。
💻 5. C++ 实现
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Manacher {
public:
/**
* 找到字符串 s 中最长回文子串
* @param s 输入字符串
* @return 最长回文子串
*/
string longestPalindrome(string s) {
if (s.empty()) return "";
// 1. 预处理:插入 '#' 分隔符
string processed = preprocess(s);
int n = processed.length();
// 2. 创建 P 数组,P[i] 表示以 i 为中心的最长回文半径
vector<int> P(n, 0);
int center = 0, right = 0; // 当前最右回文的中心和右边界
int maxLen = 0; // 记录最长回文的长度
int maxCenter = 0; // 记录最长回文的中心
// 3. 遍历预处理后的字符串
for (int i = 0; i < n; ++i) {
// 3.1 利用对称性初始化 P[i]
int mirror = 2 * center - i; // i 关于 center 的对称点
if (i < right) {
// i 在 center 的回文范围内
P[i] = min(right - i, P[mirror]);
} else {
// i 不在任何已知回文的覆盖范围内
P[i] = 1;
}
// 3.2 尝试扩展回文
// 注意:扩展时需要检查边界
while (i + P[i] < n && i - P[i] >= 0 &&
processed[i + P[i]] == processed[i - P[i]]) {
P[i]++;
}
// 3.3 更新 center 和 right
if (i + P[i] > right) {
center = i;
right = i + P[i];
}
// 3.4 更新最长回文记录
if (P[i] > maxLen) {
maxLen = P[i];
maxCenter = i;
}
}
// 4. 根据 P 数组的结果,提取原字符串中的最长回文
// maxLen 是回文长度(在预处理字符串中)
// 起始位置在原字符串中为 (maxCenter - maxLen) / 2
// 长度为 maxLen - 1 (因为预处理字符串中的长度比原字符串中的实际回文长度多1)
int start = (maxCenter - maxLen) / 2;
return s.substr(start, maxLen - 1);
}
private:
/**
* 预处理函数:在字符串的每个字符之间以及首尾插入 '#'
* @param s 原字符串
* @return 预处理后的字符串
*/
string preprocess(const string& s) {
string result = "#";
for (char c : s) {
result += c;
result += '#';
}
return result;
}
};
// 测试函数
int main() {
Manacher manacher;
vector<string> testCases = {"abaab", "abacabad", "racecar", "abcdef", "a", ""};
for (const string& test : testCases) {
string result = manacher.longestPalindrome(test);
cout << "输入: \"" << test << "\" -> 最长回文: \"" << result << "\"" << endl;
}
return 0;
}
📊 6. 复杂度分析
- 时间复杂度:
O(n)- 虽然有嵌套循环,但
right指针只会向右移动,最多移动n次。每个字符的扩展操作总和是O(n)。
- 虽然有嵌套循环,但
- 空间复杂度:
O(n)- 用于存储预处理后的字符串和
P数组。
- 用于存储预处理后的字符串和
✅ 7. 总结
Manacher 算法通过两个关键技巧实现了线性时间复杂度:
- 预处理:通过插入分隔符,将奇偶回文统一为奇数长度,简化了问题。
- 利用对称性:利用已知回文串的对称性,避免了对每个中心都进行完全的暴力扩展。
Manacher算法:高效寻找最长回文子串
305

被折叠的 条评论
为什么被折叠?



