瞎扯\(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. mongodb4.0 安装

    下载: wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.9.tgz 解压缩 tar -zxvf mongodb-linux ...

  2. httpsqs消息队安装

    HTTPSQS(HTTP Simple Queue Service)是一款基于 HTTP GET/POST 协议的轻量级开源简单消息队列服务,使用 Tokyo Cabinet 的 B+Tree Key ...

  3. Ajax基本概念

    一. Ajax 1. 什么是ajax Ajax: asynchronous  javascript  and  xml (异步js和xml) 其是可以与服务器进行(异步/同步)交互的技术一. ajax ...

  4. 帮你培养类型思维TypeScript(一)

    前言:作为一名程序员,相信你已经熟练掌握了JavaScript语言,由于其应用领域非常的广泛,所以算得上是每一个程序员必须要掌握的语言.但是JavaScript自身的缺点,相信每一个程序员也是深有体会 ...

  5. C++大数运算模板

    #include<iostream> #include<cstring> #include<cstdio> #include<iomanip> #inc ...

  6. 016 Android 图片选择器(在选中和未选中的过程中,切换展示图片)

    1.目标效果 在选中和未选中的过程中,切换展示图片 2.实现方法 (1)在app--->res--->drawable 右击drawable文件夹右键,new ---->drawab ...

  7. Mybatis笔记1

    Mybatis 持久层框架,数据访问层 mybatis是一个优秀的基于java的持久层框架,它内部封装了jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动,创建连接,创建sta ...

  8. C++ new/delete详解及原理

    学了冯诺依曼体系结构,我们知道: 硬件决定软件行为,数据都是围绕内存流动的. 可想而知,内存是多么重要.当然,我们这里说的内存是虚拟内存(详情看Linxu壹之型). 1.C/C++内存布局 2.C语言 ...

  9. 15. Scala并发编程模型Akka

    15.1 Akka介绍 1) Akka是Java虚拟机JVM平台上构建高并发.分布式和容错应用的工具包和运行时,可以理解成Akka是编写并发程序的框架 2) Akka用Scala语言写成,同时提供了S ...

  10. python之简单爬取一个网站信息

    requests库是一个简介且简单的处理HTTP请求的第三方库 get()是获取网页最常用的方式,其基本使用方式如下 使用requests库获取HTML页面并将其转换成字符串后,需要进一步解析HTML ...