1. 算法概述

Manacher算法是一种用于查找字符串中最长回文子串的线性时间复杂度算法。由Glenn Manacher于1975年提出,它巧妙地利用了回文串的对称性质来避免不必要的重复计算。

1.1 传统方法的问题

传统的中心扩展法需要 \(\mathcal{O}(n^2)\) 时间复杂度,因为它对每个字符都作为中心向两边扩展检查回文。Manacher算法通过记录之前计算的信息,将时间复杂度优化到 \(\mathcal{O}(n)\)。

2. 算法预处理

Manacher算法首先对原始字符串进行预处理,解决奇偶长度回文的问题。

2.1 插入特殊字符

我们在每个字符之间插入一个特殊字符(通常用 #,只要不混淆就行),并在首尾也插入特殊字符。

例如:

原始字符串: abba

预处理后: #a#b#b#a#

这样处理后,所有回文子串都变为奇数长度,统一了处理方式。

2.2 C++预处理实现

string preProcess(const string& s) {
int n = s.length();
if (n == 0) return "^$";
string result = "^";
for (int i = 0; i < n; i++) {
result += "#" + s.substr(i, 1);
}
result += "#$";
return result;
}

3. 核心概念

3.1 回文半径数组P

定义数组 \(P\),其中 \(P_i\) 表示以 \(i\) 为中心的回文串的半径(包括中心自身)。

例如:

1 2 3 4 5 6 7 8 9 10 11
处理后的字符串 \(T\) ^ # a # b # b # a # $
\(P\) 数组 \(0\) \(0\) \(1\) \(0\) \(1\) \(4\) \(1\) \(0\) \(1\) \(0\) \(0\)

3.2 中心C和右边界 \(R\)

  • \(C\): 当前已知的回文串的中心位置。
  • \(R\): 当前已知的回文串的右边界位置 \((C + P_C)\)。

4. 算法流程

4.1 初始化

  • C = 0, R = 0。
  • \(P\) 数组全部初始化为 \(0\)。

4.2 主要步骤

对于每个 \(i\) 从 \(1\) 到 \(n-1\):

  1. 计算 \(i\) 关于C的镜像 \(i\_mirror = 2\cdot C - i\)。
  2. 如果 \(i < R\),则 \(P_i = \min(R-i, P_{i\_mirror})\)。
  3. 尝试扩展以i为中心的回文串。
  4. 如果 \(i + P_i > R\),更新 \(C\) 和 \(R\)。

4.3 C++实现

vector<int> manacher(const string& s) {
string T = preProcess(s);
int n = T.length();
vector<int> P(n, 0);
int C = 0, R = 0;
for (int i = 1; i < n-1; i++) {
int i_mirror = 2 * C - i; // 计算i关于C的对称点
if (R > i)
P[i] = min(R - i, P[i_mirror]);
else
P[i] = 0;
// 尝试扩展以i为中心的回文
while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
P[i]++;
// 如果扩展后的回文右边界超过R,更新中心和右边界
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
return P;
}

5. 获取最长回文子串

5.1 从P数组中提取信息

  • 最长回文长度 = \(\max(P_i)\)。
  • 中心位置 = \(argmax(P_i)\)。

5.2 C++实现

string longestPalindrome(const string& s) {
vector<int> P = manacher(s);
int max_len = 0;
int center_index = 0;
for (int i = 1; i < P.size()-1; i++) {
if (P[i] > max_len) {
max_len = P[i];
center_index = i;
}
}
int start = (center_index - max_len) / 2;
return s.substr(start, max_len);
}

6. 复杂度分析

6.1 时间复杂度

  • 预处理: \(\mathcal{O}(n)\)。
  • 主算法: \(\mathcal{O}(n)\) (因为while循环的扩展操作总共不会超过 \(n\) 次)。
  • 总时间复杂度: \(\mathcal{O}(n)\)。

6.2 空间复杂度

  • 需要 \(\mathcal{O}(n)\) 空间存储P数组。
  • 预处理字符串需要 \(\mathcal{O}(n)\) 空间。
  • 总空间复杂度: \(\mathcal{O}(n)\)。

7. 算法正确性证明

Manacher 算法的正确性基于以下观察:

  1. 对称性质:回文串的对称性意味着我们可以利用已经计算过的信息。
  2. 边界控制:通过维护当前最右边界 \(R\),避免了重复计算。
  3. 线性扩展:每次扩展要么直接利用对称点信息,要么从 \(R\) 开始扩展,确保不重复。

8. 边界情况处理

8.1 空字符串

预处理后变为 ^$,\(P\) 数组为[0,0],返回空字符串。

8.2 单字符字符串

预处理后变为" ^#a#$,P数组为[0,0,1,0,0],返回该字符。

8.3 全相同字符

aaaa,算法仍能高效处理,\(P\) 数组会线性增长。

9. 实际应用示例

9.1 示例运行

输入: cbbd

预处理后: ^#c#b#b#d#$

P数组计算过程:

i=1: \(P_1=0 (^#)\)

i=2: \(P_2=1 (#c#)\)

i=3: \(P_3=0 (b)\)

i=4: \(P_4=1 (#b#b#)\)

i=5: \(P_5=0 (b)\)

i=6: \(P_6=1 (#d#)\)

i=7: \(P_7=0 (\))$

最长回文长度为 \(1\),对应原始字符串中的 bb

10. 算法优化技巧

10.1 空间优化

可以只存储当前需要的部分P值,但实现会变得复杂。

10.2 预处理优化

可以使用原始字符串的2n+1长度数组,避免字符串拼接。

10.3 并行计算

某些步骤可以并行化,但通常不必要因为算法已经是线性。

11. 常见错误与调试

11.1 数组越界

在扩展回文时要检查边界:

while (i + 1 + P[i] < n && i - 1 - P[i] >= 0 &&
T[i + 1 + P[i]] == T[i - 1 - P[i]]) {
P[i]++;
}

11.2 初始值错误

确保 \(P\) 数组初始化为 \(0\),\(C\) 和 \(R\) 初始化为 \(0\)。

11.3 预处理不当

确保预处理后的字符串首尾有不同字符(^$),避免边界检查。

12. 完整测试代码

#include <iostream>
#include <vector>
#include <algorithm> using namespace std; string preProcess(const string& s) {
int n = s.length();
if (n == 0) return "^$";
string result = "^";
for (int i = 0; i < n; i++) {
result += "#" + s.substr(i, 1);
}
result += "#$";
return result;
} vector<int> manacher(const string& s) {
string T = preProcess(s);
int n = T.length();
vector<int> P(n, 0);
int C = 0, R = 0;
for (int i = 1; i < n-1; i++) {
int i_mirror = 2 * C - i;
if (R > i)
P[i] = min(R - i, P[i_mirror]);
else
P[i] = 0;
while (i + 1 + P[i] < n && i - 1 - P[i] >= 0 &&
T[i + 1 + P[i]] == T[i - 1 - P[i]]) {
P[i]++;
}
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
}
return P;
} string longestPalindrome(const string& s) {
if (s.empty()) return "";
vector<int> P = manacher(s);
int max_len = 0;
int center_index = 0;
for (int i = 1; i < P.size()-1; i++) {
if (P[i] > max_len) {
max_len = P[i];
center_index = i;
}
}
int start = (center_index - max_len) / 2;
return s.substr(start, max_len);
} int main() {
vector<string> test_cases = {
"babad",
"cbbd",
"a",
"ac",
"abb",
"abcba",
"aaaa",
""
};
for (const string& s : test_cases) {
cout << "Input: \"" << s << "\"" << endl;
cout << "Longest palindrome: \"" << longestPalindrome(s) << "\"" << endl;
cout << endl;
}
return 0;
}

13. 算法扩展

13.1 查找所有回文子串

通过遍历 \(P\) 数组,可以找到所有回文子串。

13.2 回文分割问题

可以用于优化回文分割的动态规划解法。

13.3 最长双回文子串

寻找两个不相交的回文子串,长度之和最大。

14. 与其他算法比较

14.1 与动态规划比较

动态规划需要 \(\mathcal{O}(n^2)\) 时间和空间,Manacher更优。

14.2 与后缀自动机比较

后缀自动机也可以解决但实现更复杂。

14.3 与哈希+二分比较

哈希方法通常需要 \(\mathcal{O}(n\log n)\) 且可能有哈希冲突。

15. 总结

Manacher算法以其优雅的设计和高效的性能成为解决最长回文子串问题的首选方法。通过巧妙地利用回文串的对称性质和预处理技巧,它将时间复杂度从 \(\mathcal{O}(n^2)\) 降低到 \(\mathcal{O}(n)\),是算法设计中"以空间换时间"和"利用已知信息"的典范。

[学习笔记] manacher 算法的更多相关文章

  1. 学习笔记 - Manacher算法

    Manacher算法 - 学习笔记 是从最近Codeforces的一场比赛了解到这个算法的~ 非常新奇,毕竟是第一次听说 \(O(n)\) 的回文串算法 我在 vjudge 上开了一个[练习],有兴趣 ...

  2. [ML学习笔记] XGBoost算法

    [ML学习笔记] XGBoost算法 回归树 决策树可用于分类和回归,分类的结果是离散值(类别),回归的结果是连续值(数值),但本质都是特征(feature)到结果/标签(label)之间的映射. 这 ...

  3. 学习笔记——EM算法

    EM算法是一种迭代算法,用于含有隐变量(hidden variable)的概率模型参数的极大似然估计,或极大后验概率估计.EM算法的每次迭代由两步组成:E步,求期望(expectation):M步,求 ...

  4. 数据挖掘学习笔记--AdaBoost算法(一)

    声明: 这篇笔记是自己对AdaBoost原理的一些理解,如果有错,还望指正,俯谢- 背景: AdaBoost算法,这个算法思路简单,但是论文真是各种晦涩啊-,以下是自己看了A Short Introd ...

  5. 学习笔记-KMP算法

    按照学习计划和TimeMachine学长的推荐,学习了一下KMP算法. 昨晚晚自习下课前粗略的看了看,发现根本理解不了高端的next数组啊有木有,不过好在在今天系统的学习了之后感觉是有很大提升的了,起 ...

  6. Java学习笔记——排序算法之快速排序

    会当凌绝顶,一览众山小. --望岳 如果说有哪个排序算法不能不会,那就是快速排序(Quick Sort)了 快速排序简单而高效,是最适合学习的进阶排序算法. 直接上代码: public class Q ...

  7. Java学习笔记——排序算法之进阶排序(堆排序与分治并归排序)

    春蚕到死丝方尽,蜡炬成灰泪始干 --无题 这里介绍两个比较难的算法: 1.堆排序 2.分治并归排序 先说堆. 这里请大家先自行了解完全二叉树的数据结构. 堆是完全二叉树.大顶堆是在堆中,任意双亲值都大 ...

  8. Java学习笔记——排序算法之希尔排序(Shell Sort)

    落日楼头,断鸿声里,江南游子.把吴钩看了,栏杆拍遍,无人会,登临意. --水龙吟·登建康赏心亭 希尔算法是希尔(D.L.Shell)于1959年提出的一种排序算法.是第一个时间复杂度突破O(n²)的算 ...

  9. 算法笔记--manacher算法

    参考:https://www.cnblogs.com/grandyang/p/4475985.html#undefined 模板: ; int p[N]; string manacher(string ...

  10. 学习笔记——SM2算法原理及实现

    RSA算法的危机在于其存在亚指数算法,对ECC算法而言一般没有亚指数攻击算法 SM2椭圆曲线公钥密码算法:我国自主知识产权的商用密码算法,是ECC(Elliptic Curve Cryptosyste ...

随机推荐

  1. Oracle、MySQL、SQL Server、PostgreSQL、Redis 五大数据库的区别

    以下是 Oracle.MySQL.SQL Server.PostgreSQL.Redis 五大数据库的对比分析,从用途.数据处理方式.高并发能力.优劣势等维度展开: 一.数据库分类 数据库 类型 核心 ...

  2. 信息资源管理综合题之“S公司规划网络系统-内部用户需要使用的信息安全技术及其相应用途”

    一.案例:S公司是某网络设备制造商在国内的一级代理商,总部设在上海,在外高桥有一处大型的仓库,其二级经销商客户分布在全国几十座大中城市,并在北京.成都.西安和沈阳等地设立了办事处.总部实施了ERP系统 ...

  3. 树-BST基本实现

    之前的数组, 栈, 链表, 队列等都是顺序数据结构, 这里来介绍一个非顺序数据结构, 树. 树在处理有层级相关的数据时非常有用, 还有在存储数据如数据库查询实现等场景也是高频使用. 作为一种分层数据的 ...

  4. 数据库迁移的艺术:FastAPI生产环境中的灰度发布与回滚策略

    title: 数据库迁移的艺术:FastAPI生产环境中的灰度发布与回滚策略 date: 2025/05/17 21:06:56 updated: 2025/05/17 21:06:56 author ...

  5. vue devtools安装及使用

    (1)chrome商店下载 进入浏览器的设置: 或者直接进入该网址:https://chrome.google.com/webstore/search/vue devtools?hl=zh-CN (2 ...

  6. Pycomcad快速绘制参数化多段线的一种方法

    任务: 绘制出不同长度的相同型式的多段线,如上图所示,仅仅是300mm和500mm的区别,3个弯折处都一样,都是圆弧段,对于常规二次开发思路,是通过数学计算,计算出圆弧的圆心的位置,用固定的半径,绘制 ...

  7. odoo里面的动作

    来源:Odoo中的五种action都是继承自ir.actions.actions模型实现的子类,共有五种,下面会一个一个给出具体例子 1.链接Action(ir.actions.act_url):ta ...

  8. 设置IntelliJ IDEA 2021字体大小

      安装Mac版 IntelliJ IDEA 2021.3.1 (Ultimate Edition)后,就需要更改字体.IntelliJ IDEA的字体设置分为两部分:一部分是UI的字体和字号设置,另 ...

  9. Java HashMap和ConcurrentHashMap知识点梳理

    jdk 8 HashMap 扩容之后旧元素存放位置是? java 在扩容的时候会创建一个新的 Node<K,V>[],用于存放扩容之后的值,并将旧的Node数组(其大小记作n)置空:至于旧 ...

  10. SQL语句between and边界问题

       BETWEEN AND 需要两个参数,即范围的起始值a和终止值b,而且要求a<b.如果字段值在指定的[闭区间[a,b]]内,则这些记录被返回:否则,记录不会被返回. 字段值可以是数值.文本 ...