瞎扯\(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. Juniper总结

    Juniper的路由器分为两个部分——RE和PFE.不过貌似大部分路由器都分为这两个部分.... Routing Engine: 当密码授权通过之后,用户就进入了RoutingEngine中,在其中可 ...

  2. 常见问题:MySQL/B+树

    平衡二叉树 此前讲红黑树时也提到了平衡二叉树,红黑树和AVL树都是能保证树不退化的平衡二叉树,平衡二叉树采用二分思想组织数据,能大大提高单点查找数据的效率,其组装过程略. 作为对比,此处也列出平衡二叉 ...

  3. axios.js 在测试机ios7.1的iphone4中不能发送http请求解决方案

    原因:axios使用promise语法,浏览器不支持该语法 解决思路:使浏览器支持promise语法 具体代码: 安装es6-promise,npm i es6-promise -D. 在引入axio ...

  4. Deep Learning Recommendation Model for Personalization and Recommendation Systems

    这篇文章出自facebook,主要探索了如何利用类别型特征(categorical features)并且构建一个深度推荐系统.值得注意的是,文章还特别强调了工业实现上如何实现并行,也很良心地给出了基 ...

  5. js原生导出excel和csv

    ​ 严格意义来说并不是真正的excel文件,只是可以用excel打开查看而已,实际上的格式是逗号分隔文件即csv文件. 这里有几个坑要说一下: 不加Unicode的utf8头部标识excel打开文件会 ...

  6. LeetCode 897. 递增顺序查找树(Increasing Order Search Tree)

    897. 递增顺序查找树 897. Increasing Order Search Tree 题目描述 给定一个树,按中序遍历重新排列树,使树中最左边的结点现在是树的根,并且每个结点没有左子结点,只有 ...

  7. 【HC89S003F4开发板】 4端口消抖

    HC89S003F4开发板端口消抖 一.前言 看到资料里有中断消抖的例子,因为以前项目里有遇到高频干扰频繁退出休眠的情况,所以好奇用这个配置能不能解决. 二.对demo进行修改 @实现效果 P01设置 ...

  8. 二十二、DMA驱动

    一.DMA简介 DMA(Direct Memory Access,直接内存存取),DMA传输将数据从一个地址空间复制到另外一个地址空间.传输过程由DMA控制器独立完成,它并没有拖延CPU的工作,可以让 ...

  9. 使用应用编排服务一键式部署,持续集成利器--jenkins

    这篇文章主要是来聊一聊jenkins,可说道jenkins,我没有办法不把它与持续集成(Continuous integration,简称CI)联系到一起,所以我先来谈谈什么是持续集成以及为什么需要持 ...

  10. vue 等比例截图组件,支持缩放和旋转

    <template> <div class="crop-image" :style="wrapStyle"> <img :src= ...