「笔记」manacher 算法
写在前面
才发现好久没写知识笔记了……
神兵小将真好看,感觉好像年轻了十岁,有一种莫名的沉浸式的体验。
还记得当年特别喜欢那个粉毛来、、、小学生特有的性冲动、、、
简介
暴力 求回文子串方法非常简单,枚举回文子串中心向两侧扩展即可,注意特判偶数长度的回文子串。
而 manacher 算法在暴力的基础上,利用了已求得的回文半径加速了比较的过程,使算法可以时空间复杂度均为 级别下完成。
算法流程
以下以模板题:P3805 【模板】manacher 算法 为例。
给定一长度为 的只由小写英文字符构成的字符串 ,求 中最长的回文子串的长度。
。
500ms,512MB。
首先在原串的开头、末尾和相邻字符间加入分隔符,使得串长度变为 。原串和新串中的回文子串均一一对应,且新串中的回文子串都是有中心奇数长度的串。
考虑在枚举回文子串中心 时维护一个数组 , 表示以 为中心的最长回文子串的半径长度。即有:
同时维护两个变量 和 。 代表以某个位置为中心能扩展到的最靠后的位置, 代表上述的位置,则显然有 。显然,对于当前枚举到的回文子串中心 ,由于 ,则更新 后至少有 ,则有 成立。
同时,我们记 代表以 为中心能扩展到的最靠前的位置。显然,由于 是一个回文串,由对称性,则对于以 为中心的某些回文子串,在 中一定存在一个 ,满足 ,且以 为中心的某些回文子串与以 为中心的某些回文子串完全相同。如下图所示:

图 1,来源:https://www.luogu.com.cn/blog/Minamoto/solution-p3805
显然,如果我们在计算以 为中心的最长回文子串时,如果可以利用 的信息 ,即可避免大量无用的扩展过程。我们考虑 的取值对 的影响:
- 如果以 为中心的最长回文子串的左端点不会越过 ,即有:,则 ,如下图所示。

还是上面的图 1,来源:https://www.luogu.com.cn/blog/Minamoto/solution-p3805
- 如果以 为中心的最长回文子串左端点越过了 ,即有:,则 ,如下图所示。

图 2,来源:https://www.luogu.com.cn/blog/Minamoto/solution-p3805
这时我们仅需从第 位开始以 为中心仅需扩展即可。
再考虑何时应当更新 的值。我们令 的初始值为 1,在枚举 过程中,每当计算出一个新的 ,就将 与当前的 进行比较,如果 ,则令 即可。
注意求得所有 后将其转化为原串的回文串长度。显然,对于以新串中位置 为中心的最长回文子串,对应原串中对应位置长度为 的最长回文子串。
代码如下。为了简便记忆,代码中并没有将上述情况 1、2 分开编写,而是均是采用了给 赋初始值后再尝试扩展的写法,显然正确性不受影响。
复制复制//By:Luckyblock /* */ #include <cstdio> #include <cctype> #include <cstring> #include <algorithm> const int kN = 1e7 + 1e6 + 10; //============================================================= int n, p[kN << 1]; char s[kN], t[kN << 1]; //============================================================= inline int read() { int f = 1, w = 0; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = - 1; for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + ch - '0'; return f * w; } //============================================================= int main() { scanf("%s", s + 1); n = strlen(s + 1); for (int i = 1; i <= n; ++ i) t[2 * i - 1] = '%', t[2 * i] = s[i]; t[n = 2 * n + 1] = '%'; int pos = 0, r = 0; for (int i = 1; i <= n; ++ i) { p[i] = 1; if (i < r) p[i] = std::min(p[2 * pos - i], r - i + 1); while (i - p[i] >= 1 && i + p[i] <= n && t[i - p[i]] == t[i + p[i]]) { ++ p[i]; } if (i + p[i] - 1 > r) pos = i, r = i + p[i] - 1; } int maxp = 0; for (int i = 1; i <= n; ++ i) maxp = std::max(maxp, p[i] - 1); printf("%d\n", maxp); return 0; }
复杂度证明
考虑暴力扩展的过程。发现暴力扩展仅会发生在 ,或是 时,且都是从 开始扩展。
- 当 时,暴力扩展后必有 ,则必定引起 的右移,右移次数不小于 ,即暴力扩展次数 。
- 当 时,如果暴力扩展成功,则一定会引起 的右移。右移次数同样等于暴力扩展次数 ;如果不成功,则 不变。
而 至多仅会更新 次,暴力扩展次数与之同阶也为 级别。同样地, 仅会右移 次,则算法的总复杂度为 级别。
写在最后
学完发现这东西好水。
为什么之前不学?因为我觉得 的哈希+二分也挺好/cy
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】