起由是刷代码随想录的DP,做到647问题,结果用DP算法时间上只打败了55.8%,想到可能有复杂度更低的算法。
由于DP和中心拓展都是O(n^2)的算法,而回文串至少是要遍历一遍的,因此,最优解应该是线性或者nlogn的时间复杂度的——即Manacher算法,又被戏称为马拉车算法。
马拉车算法可以看作是中心拓展的一种改良。
对于字符串abbaa,若用中心拓展,那有两种中心选择方式
1. 以单字符为中心
2. 以双字符为中心
但是马拉车算法首先优化了中心选择的方式,向字符串相邻空间插入"#"将所有字符串长度变为奇数个以单字符为中心。
Manacher 算法的几点说明
- 记f(i) 为 s 的第 i 位的回文半径长度,那么容易证明 f(i)-1就是以下标 i 为中心的最大回文子串长度(这个证明考虑插入的#个数就可以想明白了)
- 那么如果计算 f(i) 那么就说明[1, i-1] 区间范围内都已经计算过了,那么如果在[1,i-1]中有一个点的半径长度包括到了 i ,那么我们就可以利用上i 关于这个半径的中点的对称点的相关回文信息,因此我们就需要维护一个最长右端点和达到该右端点的中心点。
- 按找上面的思路,那如果 i 都不在最长右端点,那也就是说我们利用不到之前回文的信息,那就直接以 i 为中心暴力中心拓展咯
这里是该题相应的代码,前面在初始化字符串的时候加上了 $ , 不然后面求 f(i) 的时候会出现问题
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
string t = "$#";
for (const char &c: s) {
t += c;
t += '#';
}
n = t.size();
t += '!';
auto f = vector <int> (n);
int iMax = 0, rMax = 0, ans = 0;
for (int i = 1; i < n; ++i) {
// 初始化 f[i]
// rMax - i + 1是他到边的长度,f[2 * iMax - i]是他对称点的最长回文子串长度;
// 由于只遍历到[1,i-1]于是在i后面的情况不能确定,因此要始终保证在最长边范围内
// 于是我们取小;
f[i] = (i <= rMax) ? min(rMax - i + 1, f[2 * iMax - i]) : 1;
// 中心拓展
while (t[i + f[i]] == t[i - f[i]]) ++f[i];
// 动态维护 iMax 和 rMax
if (i + f[i] - 1 > rMax) {
iMax = i;
rMax = i + f[i] - 1;
}
// 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
ans += (f[i] / 2);
}
return ans;
}
};