前缀函数及 Knuth–Morris–Pratt 算法学习笔记
\(\text{1 引言 Preface}\)
对于形如以下的问题:
给予一个模式串 \(T\) 和主串 \(S\),在主串中寻找 \(T\)。
我们称之为字符串匹配。
很显然朴素算法时间复杂度是 \(O(n^2)\) 的:
枚举字符串起点,向后逐位比较。
所以需要对其进行优化,一般使用 \(\text{hash}\) 或者 \(\text{Knuth–Morris–Pratt}\)(下文简称为 \(\text{KMP}\))算法,而 \(\text{hash}\) 在本文不进行讨论。而 \(\text{KMP}\) 即为一种可以在 \(O(|S|+|T|)\) 的时间复杂度以及 \(O(|S|)\) 的空间复杂度下完成这件事的优秀算法。
\(\text{2 核心 Main Idea}\)
\(\text{2.1 前缀函数 }\pi(x)\text{ The function of prefix}\)
\(\text{2.1.1 定义 Definition}\)
对于主字符串 \(S\),我们称后缀函数 \(\pi(x)\) 为该字符串长度为 \(x\) 的前缀的所有的真前缀中等于真后缀的最长长度。是的这是它冗长的定义,转化为数学语言就是如下:
记字符串 \(A\) 从左往右数第 \(i\) 位为 \(A_i\)。令字符串 \(P\) 为 \(S\) 长度为 \(x\) 的前缀,即 \(P= S_{1,2,\cdots,x}\),那么 \(\pi(x)\) 等于满足 \(P_{1,2,\cdots,y}=P_{x-y+1,x-y+2,\cdots,x}\) 的最大的 \(y\) 值。
\(\text{2.1.2 朴素求解方式 The simple way to calculate}\)
注:下文中的“求解”指的均是对于串 \(S\),求解出每个 \(\pi(i)\) 的值,其中 \(i=1,2,\cdots,|s|\)。
一个很显然的求解方式就是先枚举前缀长度 \(x\),然后枚举 \(y\) 值,然后截取字符串前后缀逐位比较,时间复杂度为 \(O(n^3)\)。
实现:
char S[N]; int pi[N];
void Prefix_Func(int n) { // 此处字符串下标由 1 开始
for (int i = 1; i <= n; i ++) {
for (int j = 1; j < i; j ++) {
bool f = true;
for (int k = 1; k <= j; k ++)
if (S[k] != S[i - j + k]) // 失配
f = false;
if (f) pi[i] = j;
}
}
return ;
}
\(\text{2.1.3 第一类优化 The first optimization}\)
若我们现在正在处理字符串 \(S\) 的到第 \(i\) 位的前缀的 \(\pi(i)\) 值,假设我们已经处理完了 \(\pi(1),\pi(2),\cdots,\pi(i-1)\),则可见的,前缀从 \(i-1\) 位增加到 \(i\) 位,则 \(\pi(i)\) 值至多会增加 \(1\)(当且仅当 \(S_i=S_{\pi(i-1)+1}\)),或者不变,或者减少。那么我们求解 \(pi(i)\) 可以如下实现:
char S[N];
int pi[N];
void Prefix_Func(int n) { // 此处字符串下标由 1 开始
for (int i = 1; i <= n; i ++) {
for (int j = min(pi[i - 1] + 1, i - 1); j >= 1; j --) {
bool f = true;
for (int k = 1; k <= j; k ++)
if (S[k] != S[i - j + k]) // 失配
f = false;
if (f) pi[i] = j;
}
}
return ;
}
此时时间复杂度为 \(O(n^2)\)。
\(\text{2.1.4 第二类优化 The second optimization}\)
下文中,我们记 \(S[i,j]\) 表示字符串 \(S\) 从第 \(i\) 个字符开始到第 \(j\) 个字符的子串。
以下我们讨论的情况均是以处理完毕 \(\pi(1),\pi(2),\cdots,\pi(i)\),然后希望求出 \(\pi(i+1)\) 的情形。
观察第一类优化,我们发现只有在 \(S_{\pi(i)+1}=S_{i+1}\) 时 \(\pi(i+1)\) 能很快求出,那么我们继续讨论不等于的情况。
当我们发现失配时,我们希望很快能又找到一个最大的 \(j<\pi(i)\),满足 \(S[1,j]=S[i-j+1,i]\),然后与上文类似地判断 \(S[j+1]\) 是否等于 \(S[i+1]\),如果是,则 \(\pi[i+1]=j+1\)。问题就在于如何快速求出 \(j\)。首先,由于 \(j<\pi(i)\) 那么由 \(\pi(i)\) 的定义可以得出 \(S[1,j]=S[i-j+1,i]\),进一步地,我们发现 \(S[1,\pi(i)]=S[i-\pi(i)+1,i]\),所以可以得到 \(S[i-j+1,i]=S[\pi(i)-j+1,\pi(i)]\),综上我们得出了 \(S[1,j]=S[i-j+1,i]=S[\pi(i)-j+1,\pi(i)]\),即 \(j=\pi(\pi(i))\)。
于是,我们可以这样求解函数值:
令 \(j_0=pi(i)\)。
判断 \(S_{i+1}\) 是否等于 \(S_{j+1}\),若是,则 \(\pi(i+1)=j+1\),否则进入下一步。
令 \(j_1=\pi(j_0)\),若其等于 \(0\),则 \(\pi(i+1)=0\),否则以类似的方式重复 1 和 2 两个步骤。
这样我们便得到了一种能在 \(O(n)\) 时间内求解前缀函数的算法了。
void Prefix_Func(int n, char* S) { // 此处字符串下标由 1 开始
for (int i = 1; i <= n; i ++) {
int j = pi[i];
while (j) {
if (S[j + 1] == S[i + 1]) break;
j = pi[j];
}
if (S[j + 1] == S[i + 1]) pi[i + 1] = j + 1;
}
return ;
}
\(\text{2.2 Knuth–Morris–Pratt 算法 The KMP Algorithm}\)
\(\text{KMP}\) 算法是前缀函数的一个巧妙的应用。
原问题为:在文本串 \(S\) 中寻找模式串 \(T\)。
我们令 \(n=|S|\),\(m=|T|\)。那我们拼接一个新的字符串 \(P=T+C+S\),其中 \(C\) 为分隔符,在 \(S\) 和 \(T\) 中均为出现。则 \(|C|=n+m+1\)。
我们先求出 \(C\) 每一位的前缀函数值,然后考虑属于字符串 \(S\) 的部分,即 \(C[n+2,n+1+m]\)。我们假设当前处理的是第 \(i\) 位,我们有当 \(\pi(i)=m\) 时字符串 \(T\) 便在 \(S\) 中出现了一次。
原因是:首先,由于分隔符 \(C\) 的存在,任何一个 \(\pi(i)\) 都小于等于 \(m\),因为 \(C\) 永远无法匹配,当 \(\pi(i)=m\) 时,由于前缀函数的定义,我们有 \(C[1,m]=C[i-m+1,i]\),又因为 \(C[1,m]\) 就是 \(T\),所以 \(C[i-m+1,i]=T\),\(T\) 在 \(C\) 的后部出现了一次,即在 \(S\) 中出现了一次,进一步地,\(C[i-m+1,i]=S[i-2\times m,i-m-1]\),所以这一次 \(T\) 出现在了 \(S\) 从左向右第 \(i-2\times m\) 位。
\(\text{3 实现 Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
// pi(x)
char S[N], T[N], P[N];
int pi[N], n, m;
void Prefix_Func(int n, char* S) { // 此处字符串下标由 1 开始
for (int i = 1; i <= n; i ++) {
int j = pi[i];
while (j) {
if (S[j + 1] == S[i + 1]) break;
j = pi[j];
}
if (S[j + 1] == S[i + 1]) pi[i + 1] = j + 1;
}
return ;
}
// \pi
int main() {
ios :: sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> (S + 1) >> (T + 1);
n = strlen(S + 1), m = strlen(T + 1);
for (int i = 1; i <= m; i ++) P[i] = T[i];
P[m + 1] = '#';
for (int i = 1; i <= n; i ++) P[i + m + 1] = S[i];
Prefix_Func(n + m + 1, P);
for (int i = m + 2; i <= n + m + 1; i ++) {
if (pi[i] == m) cout << i - 2 * m << "\n";
}
for (int i = 1; i <= m; i ++)
cout << pi[i] << " ";
return 0;
}
\(\text{4 后记}\)
总计花了约四五个小时完成此文,时间跨越了约三天,\(\text{KMP}\) 是我很久以前就知道但一直没有去学的算法之一,我突然想到学他是因为模拟赛出了一道 \(\text{Manacher}\) 套 \(\text{KMP}\) 以及前几天的 \(\text{ABC362}\) 出了一题板子 \(\text{AC}\) 自动机,但我赛时是贺题解过的。
前缀函数及 Knuth–Morris–Pratt 算法学习笔记的更多相关文章
- 我所理解的 KMP(Knuth–Morris–Pratt) 算法
假设要在 haystack 中匹配 needle . 要理解 KMP 先需要理解两个概念 proper prefix 和 proper suffix,由于找到没有合适的翻译,暂时分别称真实前缀 和 真 ...
- KMP 算法(Knuth–Morris–Pratt algorithm)的基本思想
KMP 算法(Knuth–Morris–Pratt algorithm)的基本思想 阅读本文之前,您最好能够了解 KMP 算法解决的是什么问题,最好能用暴力方式(Brute Force)解决一下该问题 ...
- 字符串匹配算法--KMP字符串搜索(Knuth–Morris–Pratt string-searching)C语言实现与讲解
一.前言 在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置.此算法通过运用对这个词在不匹配时本身就包含足够的信息 ...
- 某科学的PID算法学习笔记
最近,在某社团的要求下,自学了PID算法.学完后,深切地感受到PID算法之强大.PID算法应用广泛,比如加热器.平衡车.无人机等等,是自动控制理论中比较容易理解但十分重要的算法. 下面是博主学习过程中 ...
- 算法学习笔记(3): 倍增与ST算法
倍增 目录 倍增 查找 洛谷P2249 重点 变式练习 快速幂 ST表 扩展 - 运算 扩展 - 区间 变式答案 倍增,字面意思即"成倍增长" 他与二分十分类似,都是基于" ...
- Miller-Rabin 与 Pollard-Rho 算法学习笔记
前言 Miller-Rabin 算法用于判断一个数 \(p\) 是否是质数,若选定 \(w\) 个数进行判断,那么正确率约是 \(1-\frac{1}{4^w}\) ,时间复杂度为 \(O(\log ...
- C / C++算法学习笔记(8)-SHELL排序
原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...
- Manacher算法学习笔记 | LeetCode#5
Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...
- Johnson算法学习笔记
\(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...
- Johnson 全源最短路径算法学习笔记
Johnson 全源最短路径算法学习笔记 如果你希望得到带互动的极简文字体验,请点这里 我们来学习johnson Johnson 算法是一种在边加权有向图中找到所有顶点对之间最短路径的方法.它允许一些 ...
随机推荐
- OpenQA.Selenium.WebDriverException The HTTP request to the remote WebDriver server for URL timed out
OpenQA.Selenium.WebDriverException:"The HTTP request to the remote WebDriver server for URL htt ...
- [C#] 禁用控制台关闭按钮
禁用控制台关闭按钮 internal class Program { [DllImport("user32.dll", EntryPoint = "FindWindow& ...
- CF1836
A.Destroyer 开个桶记录个数,看满不满足单调不上升即可. B.Astrophysicists 辛辛苦苦写了这么久的文章就没了????烦死了. 自己做 Virtual Contest 的时候这 ...
- 剑指Offer-59.按之字形顺序打印二叉树(C++/Java)
题目: 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推. 分析: 实际上是二叉树的层次遍历,只不过每一行 ...
- oop课程4-6次作业小结
目录 (1)前言 (2)设计与分析 第四次作业(答题判题程序-4) 新增多选类 新增填空类 第五次作业(家居强电电路模拟程序-1) Element类 控制设备 开关# 分档调速器# 受控设备 白炽灯# ...
- JAVA发送邮件报错: 535 Error: authentication failed, system busy。
解决方法: 1.设置 -> 微信绑定 -> 开启安全登录 -> 生成新密码 2.使用生成的新密码替换邮箱登录密
- Java freemarker生成word模板文件(如合同文件)及转pdf文件方法
Java freemarker生成word模板文件(如合同文件)及转pdf文件方法创建模板文件ContractTemplate.docx ContractTemplate.xml 导入的Jar包 co ...
- spring数据验证
一般情况下,我们并不推荐在服务端做基础的数据校验,因为这有一个很主要的问题:它加重了服务器的负载,如果并发多,这种负载就更加明显. 如果我们跟踪一个简单的Controller方法执行过程,就会发现Sp ...
- 天翼云安装nexus3.37.1
有点操蛋,官网网络太慢了! 百度了不少网友的内容,综合如下 总体是个皮毛,但已经可以用于开发了! 一.下载和安装 https://download.sonatype.com/nexus/3/nexus ...
- STM32 CubeMX 学习:002-外部中断的使用
背景 上一讲 STM32 CubeMX 学习:GPIO的使用 介绍了如何配置以及操作GPIO引脚. 这一讲我们通过中断来控制按键.关于中断的概念不做介绍. HOST-OS : Windows-10 S ...