KMP 学习(复习)笔记

KMP(Knuth-Morris-Pratt)是算法竞赛中常用的字符串匹配算法之一,它可以有效地利用失配信息来使得匹配全过程中不回溯,从而在线性时间内完成匹配。

本文已有前置算法讲解(来自刘毅学长):Here

原理

设模式串 pattern 为 "utqqutnu",目标串 target 为 "utqlwutqqutnu",使用朴素算法进行匹配时("-" 表示匹配成功,"|" 表示在此字符失配):

utqqutlwutqqutnu
------|
utqqutnu

首先,将两串首部对齐,逐个字符匹配,可见在字符 'l' 处失配,按照朴素算法的思想,我们需要把模式串右移一个字符,然后再从模式串首部开始匹配,即:

utqqutlwutqqutnu
|
utqqutnu

这时发现从第一个字符起就不匹配,还要继续右移 ……

但是,似乎有一种更好的策略:我们可以直接把模式串的开头对齐目标串的 "ut" 处,就可以一次跳过几个字符,并且模式串无需回溯:

utqqutlwutqqutnu
--|
utqqutnu

而接下来这次失配后,本来需要将模式串与 't' 对齐,但事实上并不需要,将模式串直接与 'l' 对齐即可。

utqqutlwutqqutnu
|
utqqutnu

KMP 算法就是利用了失配后的部分匹配信息来选择模式串的移动方式,尽可能地避免无用的匹配。

失配信息的利用

通过上述例子我们可以观察到,如果部分匹配的串有对称的前后缀,则我们可以直接将模式串中部分匹配串的前缀与目标串中部分匹配串的后缀对齐,如:

utqqutlwutqqutnu
------|
utqqutnu

例子中的部分匹配串为 "utqqut",有对称的前后缀 "ut",则可以直接将目标串的第二个 "ut" 与模式串的第一个 "ut" 对齐。

再来看这个例子,模式串为 "ttitty",目标串为 "ttittitty"

ttittittypoi
-----|
ttitty

此时的部分匹配串为 "ttitt",它有两个对称的前后缀,分别是 "tt" 和 "t",我们会想,以 "t" 对齐,可以移动更长的距离,事实上呢?

ttittittypoi
-|
ttitty

在模式串第二个 't' 处失配后,继续匹配,最终结果是匹配失败。

然而,如果我们以 "tt" 对齐,则有:

ttittittypoi
------
ttitty

结果是匹配成功。

这个例子告诉我们,当部分匹配串有多个对称前后缀时,需要选择最长的,以保证匹配结果的正确。

失配信息的推导

事实上,KMP 算法利用的失配信息是与目标串无关的,它仅与模式串有关,我们可以用递推的方法在线性的时间内求出模式串的每个可能的部分匹配串(即所有前缀)前缀的失配信息。

我们定义 fail 数组是一个长度等于模式串长度的数组,它的第 i 个成员代表以模式串前 i 个字符作为部分匹配串时,部分匹配串的最长对称前后缀长度。

| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
target| u | t | q | q | u | t | n | u
fail | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 1

推导 fail[i] 的方法如下:

  1. 如果 fail[i - 1] 不为 0,且第 i 个字符与第 fail[i - 1] + 1 个字符相同,则 fail[i] 即为 fail[i - 1] + 1;
  2. 如果 fail[i - 1] 为 0,且第 i 个字符与首个字符相同,则 fail[i] = 1,否则 fail[i] = 0;
  3. 难点:如果 fail[i - 1] 不为 0,且第 i 个字符与第 fail[i - 1] + 1 个字符不同,则继续对比第 i 个字符与 fail[fail[i - 1]] + 1 个字符,一直向前找直到匹配或者找到了 0。

如模式串:agctagcagctagct

加粗的 'a' 与最后一个 't' 不匹配,此时向前找找到 "agctagc" 的最后一个 'c' 的对称位置的后一个字符,发现是 't',则找到前后的 "agct" 是一个对称的前后缀。

匹配

有了 fail 数组,匹配就简单多了,只要根据以下三种情况对应处理即可:

  1. 如果当前字符匹配,则继续匹配下一个字符;
  2. 如果当前在模式串的首字符处不匹配,则直接将模式串右移一个字符;
  3. 否则移动模式串,使模式串中部分匹配串的前缀与目标串中部分匹配串的后缀对齐。

完整代码(POJ 3461)

更新于 2021年03月04日

#include <cstdio>
#include <cstring> const int MAXN = 1000000; inline int kmp(char *a, char *b) // 在 a 中寻找 b
{
// 求出字符串长度
int na = strlen(a + 1), nb = strlen(b + 1);
static int fail[MAXN + 1]; fail[1] = 0;
for (int i = 2; i <= nb; i++) {
// 取上一位置的 fail 位置之后的字符,判断是否和该位相同
int j = fail[i - 1];
// 不断地向前找 fail 位置,直到找到 0 位置或可以匹配当前字符
while (j != 0 && b[j + 1] != b[i]) j = fail[j]; // 如果能匹配,设置当前位置的 fail 位置
if (b[j + 1] == b[i])
fail[i] = j + 1;
else
fail[i] = 0; // 找不到匹配位置
} int res = 0; // 匹配次数
for (int i = 1, j = 0; i <= na; i++) {
// 取上一位置的 fail 位置之后的字符,判断是否和要匹配的字符相同
while (j != 0 && b[j + 1] != a[i]) j = fail[j]; // 这一位可以匹配上
if (b[j + 1] == a[i]) j++; // 匹配成功
if (j == nb) {
res++;
j = fail[j]; // 为了能匹配重叠串
// j = 0 // 如果不允许重叠匹配
}
} return res;
} int main() {
int T = 1;
for (scanf("%d", &T); T--;) {
static char a[MAXN + 2], b[MAXN + 2];
// 下标从 1 开始
scanf("%s %s", a + 1, b + 1);
printf("%d\n", kmp(b, a));
}
return 0;
}

KMP 复习笔记的更多相关文章

  1. Java基础复习笔记系列 九 网络编程

    Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...

  2. Java基础复习笔记系列 八 多线程编程

    Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...

  3. Java基础复习笔记系列 七 IO操作

    Java基础复习笔记系列之 IO操作 我们说的出入,都是站在程序的角度来说的.FileInputStream是读入数据.?????? 1.流是什么东西? 这章的理解的关键是:形象思维.一个管道插入了一 ...

  4. Java基础复习笔记系列 五 常用类

    Java基础复习笔记系列之 常用类 1.String类介绍. 首先看类所属的包:java.lang.String类. 再看它的构造方法: 2. String s1 = “hello”: String ...

  5. Java基础复习笔记系列 四 数组

    Java基础复习笔记系列之 数组 1.数组初步介绍? Java中的数组是引用类型,不可以直接分配在栈上.不同于C(在Java中,除了基础数据类型外,所有的类型都是引用类型.) Java中的数组在申明时 ...

  6. Java基础复习笔记基本排序算法

    Java基础复习笔记基本排序算法 1. 排序 排序是一个历来都是很多算法家热衷的领域,到现在还有很多数学家兼计算机专家还在研究.而排序是计算机程序开发中常用的一种操作.为何需要排序呢.我们在所有的系统 ...

  7. Angular复习笔记7-路由(下)

    Angular复习笔记7-路由(下) 这是angular路由的第二篇,也是最后一篇.继续上一章的内容 路由跳转 Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为.对于使 ...

  8. Angular复习笔记7-路由(上)

    Angular复习笔记7-路由(上) 关于Angular路由的部分将分为上下两篇来介绍.这是第一篇. 概述 路由所要解决的核心问题是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL来表 ...

  9. Angular复习笔记6-依赖注入

    Angular复习笔记6-依赖注入 依赖注入(DependencyInjection)是Angular实现重要功能的一种设计模式.一个大型应用的开发通常会涉及很多组件和服务,这些组件和服务之间有着错综 ...

  10. Angular复习笔记5-指令

    Angular复习笔记5-指令 在Angular中,指令是一个重要的概念,它作用在特定的DOM元素上,可以扩展这个元素的功能,为元素增加新的行为.本质上,组件可以被理解为一种带有视图的指令.组件继承自 ...

随机推荐

  1. 2021年前端面试题——JS

    目录: DOM事件流有那些阶段? 解释事件冒泡以及如何阻止它? 事件委派/事件委托是什么? 如何理解 JS 中的this关键字? 更改this指向的方法有那些? apply.call.bind 区别? ...

  2. JAVAweek6

    本周学习 Java语言基础:运算符[有所区别] 算术运算符 +(3+2) - * / %(取余,模运算) +(连接符)(3+''2'') ++ -- class VarDemo { //算术运算符 p ...

  3. 数据分析人员需要掌握sql到什么程度?

    SQL(Structured Query Language)是用于管理和操作关系型数据库的标准化语言,对于数据分析人员来说,掌握SQL是至关重要的. 在本文中,我们将详细探讨数据分析人员需要掌握SQL ...

  4. H5自适应

    一.设置html的font-size,使用rem作为单位 假设设计稿宽度750px,屏幕宽高750px, 1.1rem=屏幕宽度/设计稿宽度*100px,适合用px表示宽度 1rem=100px re ...

  5. Linux笔记03: Linux常用命令_3.4文件和目录共用命令

    3.4 目录和文件共用命令 3.4.1 rm命令 ●命令名称:rm. ●英文原意:remove files or directories. ●所在路径:/usr/bin/rm. ●执行权限:所有用户. ...

  6. 后端程序员必会的前端知识-02:JavaScript

    第二章. Javascript 它是一种脚本语言,可以用来更改页面内容,控制多媒体,制作图像.动画等等 例子 修改页面内容 js 代码位置 <script> // js 代码 </s ...

  7. SpringBoot 异步编程浅谈

    1. 需求背景 当我们需要提高系统的并发性能时,我们可以将耗时的操作异步执行,从而避免线程阻塞,提高系统的并发性能.例如,在处理大量的并发请求时,如果每个请求都是同步阻塞的方式处 理,系统的响应时间会 ...

  8. 如何对连续型数据进行离散化处理,并进行OneHot编码?

    如何对连续型数据进行离散化处理,并进行OneHot编码,最终将OneHot编码作为特征因子输入模型? 什么是OneHot编码 One-Hot编码是分类变量作为二进制向量的表示.这首先要求将分类值映射到 ...

  9. requests.exceptions.ProxyError问题解决方法

    出现这个问题是因为你系统上在使用代理,然后你的代理又是规则匹配的. https://stackoverflow.com/questions/36906985/switch-off-proxy-in-r ...

  10. 利用腾讯快捷登录协议截取 QQ ClientKey / QQKey 实战课程

    本文主要通过利用腾讯网页快捷登录协议来模拟访问并截取已登录 QQ 客户端的Token.Uin.ClientKey.Skey.P_skey等. Step 1. https://ssl.xui.ptlog ...