瞎扯\(KMP\)

众所周知,\(KMP\)是一种玄学的字符串模式匹配算法。

  • 什么是字符串模式匹配?

通俗的讲,就是统计一个字符串(通常很长)中某个子串(即一段连续的字符)出现的次数或位置。一般来说,我们把需要进行统计的那个很长的字符串叫做文本串,把要查找的子串叫模式串。字符串模式匹配,顾名思义,就是在文本串里面匹配模式串的意思。

  • 从暴力引入

很容易想到解决字符串模式匹配的一种暴力的解法,就是枚举文本串每个位置,看看它后面几位是否完全与模式串匹配,可以结合字符串hash一起做。这种做法复杂度是\(O(nm)\)的。

有没有更好的办法呢?当然,文题不就是吗。

  • 什么是\(KMP\)?

不要在意这个名字。

其实\(KMP\)的做法大体上与上面提到的暴力一致,都是线性的枚举和匹配,不过是用了一些东西优化。

用了什么来优化(不过是一些名词,理解其思想才是最重要的):

  1. 前缀、后缀,前缀、后缀。
  2. 一个辅助数组,通常叫\(next\)数组

它们是这样定义的:

前缀、后缀:

对于这样的一个字符串:

abcda

它的前缀是:a, ab, abc, abcd, abcda

它的后缀是:e, de, cde, bcde, abcda

加个真的意思就是不包含字符串本身的前缀、后缀。

\(next\)数组比较难理解,我们先思考一个问题。

假设对于任意的一个字符串X,我们用一个模式串A去匹配。先试着按照暴力的思路做下去,比如从第一个字符开始,逐一将文本串和模式串进行比对,向后枚举了一些字符之后,我们遇到第一个失配字符(文本串与模式串匹配时不相同的字符),暴力的思路是从这个失配字符开始,又从模式串起点开始用模式串去匹配文本串

试着做一些假设,在失配时,我们是否可以不必从模式串的起点重新开始匹配呢?我们是否可以从模式串的在起点后面的位置开始匹配?这样就可以尽可能节省时间。

\(KMP\)算法正是以此为突破口(我猜的),搞出来的。

回到上面的话题,\(next\)数组就是用来干这件事的,也就是从模式串的在起点后面的位置开始匹配,不过它比较巧妙。

\(next\)数组有如下定义:

对于一个字符串\(A[1\sim n]\),对于其中任意的位置\(i\),必然存在一个位置\(j\),且\(j<=i,j!=1\),\(A[i]=A[j]\),使得\(A[1]\sim A[j-1]\)与\(A[i-j+1]\sim A[i-1]\)每一位都相同,此时我们令\(next[i]=j\)。换句话说,其实就是对于任意字符串\(A\)的所有子串,这个位置\(j\)就代表某个前缀,与它长度相同的后缀与它完全匹配。比较费解的是,\(next[i]\)是对于字符串\(A[1\sim i]\)而言的,也就是对于总的字符串的某个前缀子串,其记录的信息意味着这个前缀子串的真前缀等价于真后缀,实际上记录的是前缀子串等价于真后缀的真前缀的末尾位置。

其实在失配时,对于模式串,之前我们是又从起点开始匹配,现在呢,我们是从失配的位置\(i\)(模式串中)变成从\(next[i]\)的位置(就是上面提到的那个\(j\)啦)开始匹配。

为啥?仔细想想,假设文本串\(A\)从位置\(i\)开始尝试与模式串\(B\)进行匹配。那么在失配之前,文本串从某个位置\(i\)开始与模式串的某个从头开始的子串肯定是完全匹配的。那么显然,对于这一段完全匹配的子串(注意此子串一定不等价于模式串且比模式串短),假设它结尾的位置为\(j\),那么\(j+1\)这个位置就是失配的。而根据我们的假设,失配之前的完全匹配的子串如果存在一个真前缀与它的真后缀完全等价,计这段真前/后缀的长度为\(x\),那么对于下一次重新开始的匹配,我们就可以从\(i+x-1\)这个位置,也就是失配之前完全匹配的子串的与真后缀等价的真前缀的末尾,开始匹配了。这就是\(KMP\)的精髓,实际上是对“任意一次失配后再次用模式串去匹配文本串该从模式串的何处开始”这个问题作了恰当的优化。

至于为什么可以这样做,由于上面提到的失配之前的子串必定是完全匹配的,那么就意味着\(next\)数组在适用于模式串时,同时适用于这段完全匹配的子串,也意味着这个子串的真前后缀等价的情况是与模式串相同的,那么显而易见,我们就可以确定模式串的该子串的一个真前缀等价于文本串的该子串真后缀,比如这个对于模式串这个真前缀的末尾位置是\(j'\),那么失配后如果我们从\(j'\)重新开始用模式串去匹配,我们可以确保从模式串的起始点到\(j'\)是与文本串完全匹配的

呼,看到这里是不是觉得我十分口胡?没事,如果没看懂你可以再看几遍上面这几段话或者看一下别人的博客(逃

理解了上面的部分,我们可以考虑一个简单的优化:为了尽可能压缩枚举模式串浪费的时间,我们取的\(next\)数组应当都是与真后缀等价的最长的真前缀的末尾位置。

看很多人用”跳“这个动词形容next数组干的事情,其实我觉得不太准确,应该与暴力类比比较好理解(个人感觉)。

上面这一大堆就是对\(next\)数组的解释,是不是看着就很可怕。(因为我没有图,也没有例子)

下面举一个简单的例子。

我们有这样一个情况:

对于文本串和模式串,我们都假设起始位置为0。

文本串:abcaeabcabedd
模式串:abcab

对于模式串,它的\(next\)数组就长这样:\([0,0,0,1,2]\)。

显然匹配到位置4的时候,我们失配了。此时模式串也匹配到位置4,\(next[4]=1\),我们就从模式串的位置1重新开始匹配。此时对于文本串我们枚举到位置4。

文本串:abcaeabcabedd
模式串: abcab

我们发现位置3是一定匹配的。然后继续往后匹配。其他情况同理。

上代码:

//b是模式串,a是文本串,la是文本串长度,lb是模式串长度
j=0;
for(int i=1;i<=la;i++){
while(j&&b[j+1]!=a[i]) j=next[j];//寻找符合条件的匹配开始位置
if(b[j+1]==a[i]) j++;//匹配过程
if(j==lb){//与模式串完全匹配,输出并开始下一轮匹配
printf("%d\n",i-lb+1);
j=next[j];
}
}

然而在这之前,我们还需要预处理出\(next\)数组,根据定义,我们很容易知道暴力做法。但是我们有一种玄学做法,就是让模式串自己匹配自己(雾。

首先显而易见\(next\)数组的第一位肯定是0。(想一想,为什么)其实是我懒得解释。

计算跟上面的过程是很一致的。

int j=0;
for(int i=2;i<=lb;i++){
while(j&&b[j+1]!=b[i]) j=next[j];
if(b[j+1]==b[i]) j++;
next[i]=j;
}

复杂度是严格的\(O(n+m)\)。

瞎扯KMP的更多相关文章

  1. KMP瞎扯一下

    什么是KMP KMP俗称看毛片算法,是高效寻找匹配字串的一个算法 百度百科 KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为 ...

  2. KMP算法求解

    // KMP.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> using namespac ...

  3. 简单有效的kmp算法

    以前看过kmp算法,当时接触后总感觉好深奥啊,抱着数据结构的数啃了一中午,最终才大致看懂,后来提起kmp也只剩下“奥,它是做模式匹配的”这点干货.最近有空,翻出来算法导论看看,原来就是这么简单(先不说 ...

  4. KMP算法

    KMP算法是字符串模式匹配当中最经典的算法,原来大二学数据结构的有讲,但是当时只是记住了原理,但不知道代码实现,今天终于是完成了KMP的代码实现.原理KMP的原理其实很简单,给定一个字符串和一个模式串 ...

  5. 萌新笔记——用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)

    前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽. 基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成"* ...

  6. [KMP]【学习笔记】

    Oulipo Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 36916   Accepted: 14904 Descript ...

  7. KMP算法实现

    链接:http://blog.csdn.net/joylnwang/article/details/6778316 KMP算法是一种很经典的字符串匹配算法,链接中的讲解已经是很明确得了,自己按照其讲解 ...

  8. KMP专题

    1.[HDU 3336]Count the string(KMP+dp) 题意:求给定字符串含前缀的数量,如输入字符串abab,前缀是a.ab.aba.abab,在原字符串中出现的次数分别是2.2.1 ...

  9. KMP学习之旅

    说起kmp就要从字符串的匹配说起,下面我们谈谈字符串的匹配 给定一个原字符串:bababababababababb,再给定一个模式串:bababb,求模式串是否在源字符串中出现 最简单的方法就是遍历源 ...

随机推荐

  1. Arcgis javascript api 动态图层自图层可见性设置

    Arcgis javascript api 动态图层自图层可见性设置 子图层管理 rest服务 sublayers sublayer ArcGISDynamicMapServiceLayer 本文主要 ...

  2. 【GStreamer开发】GStreamer基础教程03——动态pipeline

    本教程介绍pipeline的一种新的创建方式--在运行中创建,而不是在运行前一次性的创建结束. 介绍 在这篇教程里的pipeline并非在运行前就全部创建结束的.放松一下,这样做没有任何问题.如果我们 ...

  3. 对String对象进行JSON序列化

    对对象进行JSON序列化,会得到类似key:value的形式. 但是如果对一个String字符串进行JSON序列化会得到什么? 测试下: public class TestMain2 {     pu ...

  4. android基础---->数据保存到文件

    Android使用与其他平台类似的基于磁盘的文件系统(disk-based file systems).这篇博客将描述如何在Android文件系统上使用File的读写APIs对Andorid的file ...

  5. docker 使用阿里云镜像加速

    1.登录阿里云 2.进入控制台 3.搜索 “容器镜像服务” 下拉点击 “镜像加速器” 复制自己的私有地址 进入自己的docker宿主机器(替换下面的地址为自己的私有地址) 修改daemon配置文件/e ...

  6. LeetCode 896. 单调数列(Monotonic Array)

    896. 单调数列 896. Monotonic Array 题目描述 如果数组是单调递增或单调递减的,那么它是单调的. 如果对于所有 i<=j,A[i]<=A[j],那么数组 A 是单调 ...

  7. [转帖]Hive学习之路 (一)Hive初识

    Hive学习之路 (一)Hive初识 https://www.cnblogs.com/qingyunzong/p/8707885.html 讨论QQ:1586558083 目录 Hive 简介 什么是 ...

  8. 静态成员函数和(CPP与C结构体的区别)

    #include <iostream> using namespace std.; //这种写法只是CPP中的struct的用法,但是在C中还是不支持的. //C中的结构体不支持写方法的. ...

  9. AtomicIntegerFieldUpdater和AtomicInteger

    为什么有了AtomicInteger还需要AtomicIntegerFieldUpdater? 当需要进行原子限定的属性所属的类会被创建大量的实例对象, 如果用AtomicInteger, 每个实例里 ...

  10. 一行css代码搞定响应式布局

    在这篇文章中,我将教你如何使用 CSS Grid 来创建一个超酷的图像网格图,它将根据屏幕的宽度来改变列的数量.最精彩的地方在于:所有的响应特性被添加到了一行 css 代码中.这意味着我们不必将 HT ...