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. <HarmonyOS第一课04>应用程序框架基础

    视频链接: https://developer.huawei.com/consumer/cn/training/course/slightMooc/C101717497122909477?ha_sou ...

  2. C# 14 新增功能一览,你觉得实用吗?

    前言 今天咱们一起来看看在 C# 14 中新增的几个功能特性,是否给我们日常编码带了来便利. 前提准备 要体验 C# 14 中的新增功能,你需要安装最新的 Visual Studio 2022 版本或 ...

  3. WordPress插件:dsdiss-ai-paraphrasing(火山引擎版)文章伪原创 !一键创作!升级!

    本站原创!<文章伪原创插件(火山引擎版)>是一款基于WordPress开发的高效内容创作工具,依托火山方舟AI模型实现智能化文章伪原创处理.插件支持在后台配置API密钥与模型ID,可灵活设 ...

  4. 2003 can't connect to mysql server on

    把配置文件my.ini换成如下所示: mysql和mysql数据存放路径都是加双斜线 [mysql] # 设置mysql客户端默认字符集 default-character-set=utf8 [mys ...

  5. .NET外挂系列:6. harmony中一些实用的反射工具包

    一:背景 1. 讲故事 本来想研究一下 IL编织和反向补丁的相关harmony知识,看了下其实这些东西对 .NET高级调试 没什么帮助,所以本篇就来说一些比较实用的反射工具包吧. 二:反射工具包 1. ...

  6. 阅读类元服务开发笔记---week2

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  7. Prompt 攻击与防范:大语言模型安全的新挑战

    随着大语言模型(LLM)在企业服务.智能助手.搜索增强等领域的广泛应用,围绕其"Prompt"机制的安全问题也逐渐引起关注.其中最具代表性的,就是所谓的 Prompt Inject ...

  8. 启智树提高组day3T1 3479 : A:climb 树

    启智树提高组day3T1 3479 : A:climb 树 题目描述 DoubleDuck山是X省的著名旅游景点.这一天,淘淘慕名而来,打算爬到山顶处. DoubleDuck山的构造是十分特殊的.在这 ...

  9. java原生链利用

    java原生链利用 在上一个文章中我们利用Java原生链进行shiro的无依赖利用; 针对在没有第三方库的时候,我们该如何进行java反序列化; 确实存在一条不依赖第三方库的java反序列化利用链;但 ...

  10. 【Zookeeper】ZooKeeper集群搭建与选举原理终极指南(Docker版 + 三角色详解)

    ZooKeeper集群搭建与选举原理终极指南(Docker版 + 三角色详解) 一.环境准备(Docker版) 1. 服务器准备(3台节点) # 所有节点执行 sudo apt-get update ...