算法·理论:Manacher 笔记
\(\text{Manacher}\) 来啦!
\(\text{Manacher}\) 并没有什么前置知识,比 \(\text{KMP}\) 简单多了。
前置处理
\(\text{Manacher}\) 算法用于解决回文串相关问题,先看几个基本概念:回文中心、回文半径,这些看字面意思就能猜到。
还有一个重要问题:对于回文串,有长度为奇数或长度为偶数之分,即奇回文串和偶回文串。显然两种回文串需要分开进行处理,因为奇回文串的回文中心是一个字符,但偶回文串的回文中心是在两个相邻字符之间的,那我们看看能不能一致处理。
不难想到,既然偶回文串的的回文中心在两个相邻的字符之间,那我们不妨往每两个相邻字符之间插入一个虚拟的字符,比如 \(\texttt{\#}\)。
比如说对于偶回文串 \(\texttt{abba}\),我们将他成 \(\texttt{\#a\#b\#b\#a\#}\),这样这个偶回文串就变成了一个奇回文串,它的回文中心就变成 \(\texttt{\#}\) 了!现在所有回文串都变成奇回文串了,接下来我们就可以一致处理了。
(至于头尾为何各放一个,后文再讲)
\(\bf{Manacher}\) 算法
\(\text{Manacher}\) 算法,可以在 \(O(n)\) 的复杂度下处理出以每个字符(或两个字符之间)为回文中心的最大回文半径 \(rad[]\)。
先说明一下回文半径的定义:如果这个回文串的回文中心为 \(o\),右端点为 \(r\),那么这个回文串的回文半径 \(rad=r-o+1\),也就是说回文半径要算上回文中心。
那么我们开始吧!首先思考朴素做法,显然我们可以枚举回文中心,再不断同时往两边扩展,扩展到不同时就找到了最远的左、右端点了,这个算法叫做中心扩展算法,时间复杂度 \(O(n^2)\),代码就不放了,很好打。
同样注意到我们可以在此基础上二分回文半径,接着用子串哈希 \(O(1)\) 比较,时间复杂度降到 \(O(n\log n)\)。
会议我们的 \(\text{KMP}\) 算法是如何优化时间复杂度的:重复利用已知的信息,我在 \(\text{KMP}\) 的文章中提过,这种思想叫做增量法,同时这也是 dp 思想的体现。
那我们考虑有什么信息可以重复利用?那显然是回文啊!那回文又有什么性质呢?对称啊!所以发现如果我们之前已经扩展到这个字符过,那前面就一定有和当前的字符对称的内容,那该字符显然也会拥有前面与它对称的字符的回文半径。
比如说字符串 \(s=\texttt{babcbab}\),当我们枚举到 \(s[6]\) 时(倒数第二个字符),显然这里已经被 \(s[4]\)(中间的 \(\texttt{c}\))扩展过。由中点公式,与它对称的字符是 \(s[2\times 4-6]=s[2]\),显然我们前面已经处理出 \(rad[2]\) 了,\(rad[2]=2\),所以 \(rad[6]\) 就至少为 \(2\) 了,当然还需要从回文半径为 \(3\) 开始继续拓展。
但注意到我们只是对称到了前面计算过的点,并不保证能完全对称到整个回文子串,比如说对于字符串 \(t=\texttt{babcbad}\),在枚举到 \(s[6]\) 时(倒数第二个字符),虽然可以通过之前 \(s[4]\)(中间的 \(\texttt{c}\))对称到 \(s[2\times 4-6]=s[2]\),但是 \(rad[6]\) 却不能到 \(rad[2]\)(自己看一下是不是),为什么呢?
因为虽然回文中心可以对称过来,但是 \(s[4]\) 的 \(rad\) 不够长,\(s[7]\) 无法对称过去,所以这样做就无法保证整个回文串都能对称过去,解决方法就是只能利用以 \(s[4]\) 为回文中心的最长回文串的右端点以内的信息,也就是说 \(rad[6]\) 不能直接等于 \(rad[2]\),还要跟在以 \(s[4]\) 为回文中心的最长回文串的右端点以内的可扩展的最长长度取 \(\min\)。
形式化的,设我们所利用的回文串的回文中心为 \(o\),右端点为 \(r\),现在枚举到 \(s[i]\) 且 \(s[i]<r\)(即可以利用是以前的信息),那么:
\]
接着继续中心扩展即可。
解释:\(\min\) 的一个参数是对称过去的字符所对应的 \(rad\),由中点公式得到;而 \(\min\) 的第二个参数是在 \(r\) 及以内的可以扩展的最长长度,相信经过前面的讲解你应该也懂了。
那在枚举的过程中同时不断更新 \(o\) 和 \(r\) 即可。
看一眼代码:
int n;
char a[N],s[N<<1];
void manacher(){
// 特殊处理
int cur=0;
s[0]='@';
s[++cur]='#';
for(int i=1;i<=n;i++) s[++cur]=a[i],s[++cur]='#';
s[++cur]='!';
n=cur-1;
// 接下来就可以一致处理了
for(int i=1,o=0,r=0;i<=n;i++){
rad[i]=(i>r?1:min(rad[(o<<1)-i],r-i+1)); // 利用之前的信息
while(s[i-rad[i]]==s[i+rad[i]]) rad[i]++; // 中心扩展
if(i+rad[i]-1>r) o=i,r=i+rad[i]-1; // 更新 o 和 r
}
}
a 是原串,s 是处理过后的字符串。
先说怎么算实际原串的以 \(i\) 为回文中心的最长回文串的长度,其实就是 \(rad[i]-1\)(因为特殊处理后加了字符 \(\texttt{\#}\)),自己分类讨论一下 \(s[i]\) 是或不是 \(\texttt{\#}\),就容易推出这个式子了。
接着我们就可以解答上文的问题了,为什么头尾要各加一个 \(\texttt{\#}\)?举个例子,对于字符串 \(\texttt{bac}\),其实应转换为 \(\texttt{\#b\#a\#c\#}\),那么在枚举到 \(\texttt{a}\) 时,实际上得到的回文串是 \(\texttt{\#a\#}\),所以对于头尾的字符我们也应该做相同处理,于是前后各加一个 \(\texttt{\#}\);或者你想想,如果两边不不加,那么 \(rad=1\),于是以它为回文中心的最长回文串的长度就为 \(rad-1=1-1=0\) 了,所以要这样修正。
那为什么头尾还要加 \(\texttt{@}\) 和 \(\texttt{!}\) 呢?是为了防止越界,或者说让扩展整个串的左右端点处停下来,比方说整个串就对称时,若枚举它的回文中心,那如果不往两边加两个不同的字符,那就会一直扩展下去,那就越界了。
其他的就没有什么好说的了,注意当 \(i>r\) 时就直接从 \(1\) 开始暴力中心扩展即可。
\(\bf{Manacher}\) 复杂度
首先答案肯定是 \(O(n)\) 的,依据是字符串算法全是线性的。
\(\text{KMP}\) 知道怎么分析了,那就自己想想吧,答案在下面。
\(\color{white}\text{同样唯一需要分析的就是这个 while,其他都显然是 O(n) 的。}\)
\(\color{white}\text{每个字符至多被从它后面暴力扩展到它一次,所以只会进行 O(n) 次 while。}\)
\(\color{white}\text{综上,实际复杂度 O(n)。}\)
累啊!不过如此!
算法·理论:Manacher 笔记的更多相关文章
- 【C#代码实战】群蚁算法理论与实践全攻略——旅行商等路径优化问题的新方法
若干年前读研的时候,学院有一个教授,专门做群蚁算法的,很厉害,偶尔了解了一点点.感觉也是生物智能的一个体现,和遗传算法.神经网络有异曲同工之妙.只不过当时没有实际需求学习,所以没去研究.最近有一个这样 ...
- python聚类算法实战详细笔记 (python3.6+(win10、Linux))
python聚类算法实战详细笔记 (python3.6+(win10.Linux)) 一.基本概念: 1.计算TF-DIF TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库 ...
- Hash 算法与 Manacher 算法
目录 前言 简单介绍 简述 Hash 冲突 离散化 基本结构 普通 Hash 简述 例题 字符串 Hash 简单介绍 核心思想 基本运算 二维字符串 Hash 例题 兔子与兔子 回文子串的最大长度 后 ...
- 痞子衡嵌入式:超级下载算法(RT-UFL)开发笔记(2) - 识别当前i.MXRT型号
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是超级下载算法开发笔记(2)之识别当前i.MXRT型号. 文接上篇 <超级下载算法(RT-UFL)开发笔记(1) - 执行在不同CM ...
- 痞子衡嵌入式:超级下载算法(RT-UFL)开发笔记(3) - 统一FlexSPI驱动访问
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是超级下载算法开发笔记(3)之统一FlexSPI驱动访问. 文接上篇 <超级下载算法(RT-UFL)开发笔记(2) - 识别当前i. ...
- 痞子衡嵌入式:超级下载算法(RT-UFL)开发笔记(4) - 轮询Flash配置参数
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是超级下载算法开发笔记(4)之轮询Flash配置参数. 文接上篇 <超级下载算法(RT-UFL)开发笔记(3) - 统一FlexSP ...
- BLDC有感FOC算法理论及其STM32软硬件实现
位置传感器:旋转编码器 MCU:STM32F405RGT6 功率MOS驱动芯片:DRV8301 全文均假设在无弱磁控制的情况下 FOC算法理论 首先,我们要知道FO ...
- 【算法】Manacher算法
最长回文串问题 manacher算法是用来求解最长回文串的问题.最长回文串的解法一般有暴力法.动态规划.中心扩展法和manacher算法. 暴力法的时间复杂度为\(O(n^3)\),一般都会超时: 动 ...
- 「Manacher算法」学习笔记
觉得这篇文章写得特别劲,插图非常便于理解. 目的:求字符串中的最长回文子串. 算法思想 考虑维护一个数组$r[i]$代表回文半径.回文半径的定义为:对于一个以$i$为回文中心的奇数回文子串,设其为闭区 ...
- 串的应用与kmp算法讲解--学习笔记
串的应用与kmp算法讲解 1. 写作目的 平时学习总结的学习笔记,方便自己理解加深印象.同时希望可以帮到正在学习这方面知识的同学,可以相互学习.新手上路请多关照,如果问题还请不吝赐教. 2. 串的逻辑 ...
随机推荐
- 每天打卡一小时 第三十一天 PTA520钻石 争霸赛
第一题 源代码 #include<iostream> using namespace std; int main() { int n; cin>>n; cout<< ...
- ZYNQ:Linux添加I2C-RTC驱动
硬件情况 使用的是DS1338这款RTC时钟芯片,I2C总线对应到PS端的I2C1. 配置 内核 添加有关的驱动: 因为DS1338用的驱动与DS13307相似,一找发现是同一个配置. CONFIG_ ...
- ubuntu 安装 arm-none-eabi-gcc 的几种方式
背景 这篇文章主要是为了解决 在 Linux 中 编译 能够在 裸机上 跑的 程序 目前许多嵌入式软件软件开发在Linux平台下进行,编译效率高很多,如今天所述的gcc-arm-none-eabi常适 ...
- 实测52.4MB/s!全国产ARM+FPGA的CSI通信案例分享!
CSI总线介绍与优势 CSI(CMOS sensor parallel interfaces)总线是一种用于连接图像传感器和处理器的并行通信接口,应用于工业自动化.能源电力.智慧医疗等领域,CSI总线 ...
- Log4Net配置详解及输出自定义消息类示例
1.简单使用实例 1.1 添加log4net.dll的引用. 在NuGet程序包中搜索log4net并添加,此次我所用版本为2.0.17.如下图: 1.2 添加配置文件 右键项目,添加新建项, ...
- Notepad++ 搭建简单Java编译运行环境
简介 有时候使用Eclips进行Java相关方法的测试和验证太繁琐,经过查询实践,使用了Notepad++和JDK搭建了一个简单的编译运行环境. 搭建过程 在电脑上安装Java环境(网上教程很多,此过 ...
- CosyVoice多语言、音色和情感控制模型,one-shot零样本语音克隆模型本地部署(Win/Mac),通义实验室开源
近日,阿里通义实验室开源了CosyVoice语音模型,它支持自然语音生成,支持多语言.音色和情感控制,在多语言语音生成.零样本语音生成.跨语言声音合成和指令执行能力方面表现卓越. CosyVoice采 ...
- 直接给一个数组项赋值,Vue 能检测到变化吗?
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动: 当你利用索引直接设置一个数组项时,例如: vm.items[indexOfItem] = newValue 当你修改数组的长度时 ...
- Class 与 Style 如何动态绑定?
Class 可以通过对象语法和数组语法进行动态绑定: 对象语法: <div v-bind:class="{ active: isActive, 'text-danger': hasEr ...
- 2024 年 Visual Studio 实用插件集合
前言 在软件开发领域,选择正确的工具可以极大地提升开发效率和质量. Visual Studio作为微软推出的强大集成开发环境(IDE),通过安装合适的插件,可以进一步增强其功能,满足开发者多样化的需求 ...