\(\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))\)。

于是,我们可以这样求解函数值:

  1. 令 \(j_0=pi(i)\)。

  2. 判断 \(S_{i+1}\) 是否等于 \(S_{j+1}\),若是,则 \(\pi(i+1)=j+1\),否则进入下一步。

  3. 令 \(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 算法学习笔记的更多相关文章

  1. 我所理解的 KMP(Knuth–Morris–Pratt) 算法

    假设要在 haystack 中匹配 needle . 要理解 KMP 先需要理解两个概念 proper prefix 和 proper suffix,由于找到没有合适的翻译,暂时分别称真实前缀 和 真 ...

  2. KMP 算法(Knuth–Morris–Pratt algorithm)的基本思想

    KMP 算法(Knuth–Morris–Pratt algorithm)的基本思想 阅读本文之前,您最好能够了解 KMP 算法解决的是什么问题,最好能用暴力方式(Brute Force)解决一下该问题 ...

  3. 字符串匹配算法--KMP字符串搜索(Knuth–Morris–Pratt string-searching)C语言实现与讲解

    一.前言   在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个主文本字符串S内查找一个词W的出现位置.此算法通过运用对这个词在不匹配时本身就包含足够的信息 ...

  4. 某科学的PID算法学习笔记

    最近,在某社团的要求下,自学了PID算法.学完后,深切地感受到PID算法之强大.PID算法应用广泛,比如加热器.平衡车.无人机等等,是自动控制理论中比较容易理解但十分重要的算法. 下面是博主学习过程中 ...

  5. 算法学习笔记(3): 倍增与ST算法

    倍增 目录 倍增 查找 洛谷P2249 重点 变式练习 快速幂 ST表 扩展 - 运算 扩展 - 区间 变式答案 倍增,字面意思即"成倍增长" 他与二分十分类似,都是基于" ...

  6. Miller-Rabin 与 Pollard-Rho 算法学习笔记

    前言 Miller-Rabin 算法用于判断一个数 \(p\) 是否是质数,若选定 \(w\) 个数进行判断,那么正确率约是 \(1-\frac{1}{4^w}\) ,时间复杂度为 \(O(\log ...

  7. C / C++算法学习笔记(8)-SHELL排序

    原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...

  8. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

  9. Johnson算法学习笔记

    \(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...

  10. Johnson 全源最短路径算法学习笔记

    Johnson 全源最短路径算法学习笔记 如果你希望得到带互动的极简文字体验,请点这里 我们来学习johnson Johnson 算法是一种在边加权有向图中找到所有顶点对之间最短路径的方法.它允许一些 ...

随机推荐

  1. 你好Avalonia框架

    https://docs.avaloniaui.net/docs/getting-started/ 起因公司事业部是做移动等营业厅办理相关业务,无纸化系统的.简单的说就是以前去营业厅办理业务都需要各种 ...

  2. 互联网软件的安装包界面设计-Inno setup

    https://blog.csdn.net/oceanlucy/article/details/50033773 "安装界面太丑了,不堪入目!" "这界面应该属于20年代 ...

  3. 【Azure App Service】.NET代码实验App Service应用中获取TLS/SSL 证书 (App Service Windows)

    在使用App Service服务部署业务应用,因为有些第三方的接口需要调用者携带TLS/SSL证书(X509 Certificate),在官方文档中介绍了两种方式在代码中使用证书: 1) 直接使用证书 ...

  4. 机器学习策略篇:详解理解人的表现(Understanding human-level performance)

    理解人的表现 人类水平表现这个词在论文里经常随意使用,但现在告诉这个词更准确的定义,特别是使用人类水平表现这个词的定义,可以帮助推动机器学习项目的进展.还记得上个博客中,用过这个词"人类水平 ...

  5. redis 基础管理

    配置文件 优化redis配置文件定制 cat /nosql/redis/6379/redis.conf daemonize yes port 6379 logfile /nosql/redis/637 ...

  6. NOIP模拟61

    T1 交通 解题思路 把环视为点,对于原图中每一个点的两条入边以及两条出边分别连边. 优于保证了原图中每个点出入度都是 2 因此新图中一定由若干个偶数环所组成的. 并且对于环中一定是只能间隔着选点,因 ...

  7. ansible搭建

    ansible配置步骤 1.创建用户 2.用户提权 3.用户免密 4.cp ansible配置文件 5.配置主机清单 6.修改ansible 用户路径下的配置文件 1.创建用户(都要做) [root@ ...

  8. pandas基础--层次化索引

    pandas含有是数据分析工作变得更快更简单的高级数据结构和操作工具,是基于numpy构建的. 本章节的代码引入pandas约定为:import pandas as pd,另外import numpy ...

  9. Jmeter自动录制脚本

    1.Jmeter配置 1.1新增一个线程组 1.2Jmeter中添加HTTP代理 1.3配置HTTP代理服务器 修改端口 修改Target Cintroller(目标控制器) 修改Grouping(分 ...

  10. kong网关部署

    软件版本: Postgresql:9.6 (不使用最新版,是因为 konga 不支持) Kong:3.4.2 konga:0.14.7 (UI管理界面) ### Postgresql部署 ## doc ...