瞎扯\(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. Django 之安全篇

    一.CSRF攻击 CSRF攻击概述: CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一.其 ...

  2. JAVA操作word方法

    jacob,功能非常强大,能操作word,excel和pdf.下载地址是:http://sourceforge.net/projects/jacob-project/ 1.新建一个文档  Dispat ...

  3. redis服务操作

    端口启动服务./redis/redis-2.8.19/src/redis-server /redis/conf/r6100.conf./redis/redis-2.8.19/src/redis-ser ...

  4. JDBC缓冲池配置druid.properties

    driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mytest?characterEncoding=UTF-8 ...

  5. CentOS 7.6出现SSH登录失败的解决方法

    CentOS 7.6出现SSH登录失败的解决方案 问题重现: iterm登录 ssh vagrant@192.168.10.10 The authenticity of host '192.168.1 ...

  6. BBS项目架构

    数据库设计 用户表(用的是auth_user那张表,通过自定义表继承AbstractUser) phone 电话 avatar 头像 create_time 创建时间#外键 blog 一对一个人站点表 ...

  7. Eclipse设置每行的最大字符数

    Eclipse默认宽度是 120 个字符.如下图所示(提示:格式化快捷键Ctrl + Shift + F): 设置步骤如下: 菜单栏倒数第二项,选择Window 下拉栏最后一项,选择Preferenc ...

  8. Python的json操作

    对数据:    json = json.dumps(data)  编码  dict->string  排序sort_keys=True, 缩进indent=4, 分隔符separators=(' ...

  9. go context 源码分析

    WithCancel func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(pare ...

  10. 三元组[01 Trie计数]

    也许更好的阅读体验 \(\mathcal{Description}\) \(\mathcal{Solution}\) 有两种方法都可以拿到满分 \(Solution\ 1\) 考虑枚举\(y\) 建两 ...