Manacher 算法 是用于在 O(n) 时间复杂度内解决“最长回文子串”等问题的高效算法。它的核心思想是利用回文的 对称性,通过巧妙的扩展和对称性推导,避免了暴力算法中重复计算的浪费。
本文将通过题目 647. 回文子串 来逐步讲解如何应用 Manacher 算法解决回文子串问题。
什么是回文?
回文是指正序与逆序完全一致的字符串,例如 aba、abba。回文的长度可以是奇数或偶数:
- 奇数长度回文:如
aba,以b为中心,两侧字符对称。 - 偶数长度回文:如
abba,正好左右对称。
统一奇数与偶数回文的处理方法
为了解决奇数和偶数回文的区别,我们在字符之间插入特殊符号 #,使回文判断统一。这样,无论是奇数还是偶数长度的回文,都可以通过对称的方式处理:
- 对于奇数长度回文:以原字符为中心,两侧字符对称。
- 对于偶数长度回文:以
#为中心,两侧字符对称。
例如:
aba->#a#b#a#abba->#a#b#b#a#
代码实现
s = `#${s.split('').join('#')}#`;
定义回文半径数组 P
回文半径 表示以中心点为起点(不包括中心字符)到回文边缘的字符个数。通过这个回文半径,我们可以得知每个中心点处回文的最大扩展范围。
以字符串 #a#b#b#a# 为例,其中一个回文子串是 #b#,中文点是 b,回文半径就是 1。
代码实现
初始化数组 P,存储每个索引对应的回文半径值:
const P = new Array(s.length).fill(0); // 回文半径
return P.reduce((a, b) => a + Math.floor((b + 1) / 2), 0); // 最大扩展范围对应的回文个数
规律
手动补全回文半径值,如下表所示:
| 序号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 字符 | # | a | # | b | # | b | # | a | # |
| P | 0 | 1 | 0 | 1 | 4 | 1 | 0 | 1 | 0 |
可以观察到以下规律:
- 回文半径是原回文子串的长度:例如
#a#b#b#a#回文半径是 4,对应的原字符串abba的长度也为 4,#符号恰好补齐了剩余字符。 - 回文半径对称性:回文有对称性,中心点左右两侧的数据完全一致,两个相对称节点的回文半径也存在对称性。
计算右侧节点的回文半径值
根据回文半径的对称性,当我们知道了回文左侧节点的回文半径后,就可以推导出右侧半径的初始值,避免从头开始计算。
初始化右侧节点回文半径
通过对称性推导,我们可以得到右侧节点的回文半径初始值。中心点为 center,索引 i 对应的左侧节点是 2 * center - i,左侧节点的回文半径就是 P[2 * center - i]。
// 因为左右两侧节点到中心点的距离相等
center - left = i - center;
left = 2 * center - i;
右边界限制
对称性只保证在回文范围内,如果超出回文范围,左右节点内容不一致,就无法保证其对称性了。
例如下图,回文对称左侧节点 a 的回文半径是 3,但其半径范围超过了当前回文的范围,就需要回缩右侧节点的回文初始值,使用 right - i 限制回文半径的初始值
代码实现
if(i < right) {
P[i] = Math.min(P[2 * center - i], right - i);
}
扩展回文半径
通过初始化,我们确定了以 i 为中心点的初始回文区间是 [i - P[i], i + P[i]],在这个基础上,我们向外扩展回文半径,直到左右的值不相等为止
代码实现
// 继续扩展回文
let leftIndex = i - P[i] - 1;
let rightIndex = i + P[i] + 1;
while(leftIndex >= 0 && rightIndex < s.length && s[leftIndex] === s[rightIndex]) {
P[i]++;
leftIndex--;
rightIndex++
}
更新中心节点和右边界值
每次回文扩展后,如果右边界值超过了当前的 right,我们需要更新 center 和 right,以保证后续的计算能够利用之前的对称性,避免重新计算。
// 更新中心值、右边界值
if(i + P[i] > right) {
center = i;
right = i + P[i];
}
完整代码
function countSubstrings(s: string): number {
s = `#${s.split('').join('#')}#`;
const P = new Array(s.length).fill(0); // 回文半径
let center = 0; // 回文中心点
let right = 0; // 回文右边界值
for(let i = 0; i < s.length; i++) {
// 获取初始值
if(i < right) {
P[i] = Math.min(P[2 * center - i], right - i);
}
// 继续扩展回文
let leftIndex = i - P[i] - 1;
let rightIndex = i + P[i] + 1;
while(leftIndex >= 0 && rightIndex < s.length && s[leftIndex] === s[rightIndex]) {
P[i]++;
leftIndex--;
rightIndex++
}
// 更新中心值、右边界值
if(i + P[i] > right) {
center = i;
right = i + P[i];
}
}
return P.reduce((a, b) => a + Math.floor((b + 1) / 2), 0);
};
总结
Manacher 算法利用回文的对称性和中心扩展思想,将时间复杂度降低到 O(n),非常适合解决回文子串等相关问题。通过巧妙的更新和扩展回文半径,算法能高效地计算回文子串个数,并且避免了重复计算。