回文串检测是算法竞赛和面试中的经典问题,但很多人在面对这个问题时,第一反应往往是暴力匹配法。这种方法的直观性确实让人容易理解,但当字符串长度达到10^5级别时,O(n²)的时间复杂度会让程序变得异常缓慢。今天我们要介绍的Manacher算法(又称马拉车算法),能在O(n)时间内解决这个问题,而且代码实现出奇地简洁。
假设给你一个字符串"abacabacabb",要求找出其中最长的回文子串。最直接的方法是枚举所有可能的子串并检查是否为回文:
python复制def is_palindrome(s):
return s == s[::-1]
def longest_palindrome_naive(s):
max_len = 0
result = ""
for i in range(len(s)):
for j in range(i, len(s)):
substring = s[i:j+1]
if is_palindrome(substring) and len(substring) > max_len:
max_len = len(substring)
result = substring
return result
这种方法虽然正确,但时间复杂度高达O(n³)。稍微优化一下,使用中心扩展法可以将复杂度降到O(n²):
python复制def expand_around_center(s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return right - left - 1
def longest_palindrome_center(s):
start = end = 0
for i in range(len(s)):
len1 = expand_around_center(s, i, i) # 奇数长度
len2 = expand_around_center(s, i, i+1) # 偶数长度
max_len = max(len1, len2)
if max_len > end - start:
start = i - (max_len - 1) // 2
end = i + max_len // 2
return s[start:end+1]
即使这样优化,对于大字符串仍然不够高效。这就是Manacher算法大显身手的地方。
Manacher算法的精妙之处在于它充分利用了回文串的对称性质,避免了重复计算。算法主要分为三个关键步骤:
C:当前已知的最右回文串的中心R:该回文串的右边界P[i]:以i为中心的最长回文半径原始字符串:"abba"
处理后字符串:"#a#b#b#a#"
这样处理后,无论原始回文串长度是奇是偶,都能统一处理:
| 原始字符串 | 处理后的字符串 | 最长回文半径 |
|---|---|---|
| "aba" | "#a#b#a#" | 3 |
| "abba" | "#a#b#b#a#" | 4 |
算法的核心在于如何利用已知信息减少比较次数。对于每个位置i:
cpp复制vector<int> manacher(string s) {
string t = "#";
for (char c : s) {
t += c;
t += '#';
}
int n = t.size();
vector<int> P(n, 0);
int C = 0, R = 0;
for (int i = 1; i < n; ++i) {
int mirror = 2 * C - i;
if (i < R) {
P[i] = min(R - i, P[mirror]);
}
while (i + P[i] + 1 < n && i - P[i] - 1 >= 0 &&
t[i + P[i] + 1] == t[i - P[i] - 1]) {
P[i]++;
}
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
return P;
}
Manacher算法的时间复杂度是O(n),这看起来可能违反直觉,因为代码中有嵌套循环。关键在于:
所以整体复杂度是O(n) + O(n) = O(n)
cpp复制class Solution {
public:
string longestPalindrome(string s) {
string t = "#";
for (char c : s) {
t += c;
t += '#';
}
int n = t.size();
vector<int> P(n, 0);
int C = 0, R = 0;
int max_len = 0, center = 0;
for (int i = 1; i < n; ++i) {
int mirror = 2 * C - i;
if (i < R) {
P[i] = min(R - i, P[mirror]);
}
while (i + P[i] + 1 < n && i - P[i] - 1 >= 0 &&
t[i + P[i] + 1] == t[i - P[i] - 1]) {
P[i]++;
}
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
if (P[i] > max_len) {
max_len = P[i];
center = i;
}
}
string result;
for (int i = center - max_len; i <= center + max_len; ++i) {
if (t[i] != '#') {
result += t[i];
}
}
return result;
}
};
python复制def countSubstrings(s: str) -> int:
t = '#'.join('^{}$'.format(s))
n = len(t)
P = [0] * n
C = R = 0
for i in range(1, n-1):
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] += 1
if i + P[i] > R:
C = i
R = i + P[i]
return sum((v+1)//2 for v in P if v > 0)
对于需要将字符串分割为若干回文子串的问题,可以先用Manacher预处理出所有可能的回文区间,再用动态规划求解:
cpp复制vector<vector<bool>> preprocessPalindrome(const string& s) {
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, false));
string t = "#";
for (char c : s) {
t += c;
t += '#';
}
vector<int> P(t.size(), 0);
int C = 0, R = 0;
for (int i = 1; i < t.size()-1; ++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];
}
int start = (i - P[i]) / 2;
int end = (i + P[i] - 1) / 2;
for (int l = start, r = end; l <= r; ++l, --r) {
dp[l][r] = true;
}
}
return dp;
}
提示:在实现时,可以在字符串前后添加不同的特殊字符(如'^'和'$')来避免边界检查
原始方法:
cpp复制string t = "#";
for (char c : s) {
t += c;
t += '#';
}
更高效的方法(预分配内存):
cpp复制string t;
t.reserve(2 * s.size() + 1);
t = "#";
for (char c : s) {
t += c;
t += '#';
}
下表比较了不同方法在LeetCode测试用例上的表现:
| 方法 | 时间复杂度 | 实际运行时间(ms) | 内存消耗(MB) |
|---|---|---|---|
| 暴力法 | O(n³) | 超时 | - |
| 中心扩展法 | O(n²) | 368 | 7.1 |
| 动态规划法 | O(n²) | 412 | 21.3 |
| Manacher算法 | O(n) | 12 | 9.8 |
在解决实际问题时,Manacher算法虽然理论复杂度最优,但对于小规模输入(n<1000),中心扩展法可能更简单实用。