关于KMP算法的理解

前言

本篇博客的字符串下标是从1开始的。

引入

给出两个字符串\(A,B\),询问\(B\)是否是\(A\)的子串。

对于以上问题,我们有一个比较暴力的想法,就是一位一位去配对呀。
给出代码:

int Check(){
    for(int i=1;i+M-1<=N;i++){
        int j=0;
        while(j<M&&A[i+j]==B[j+1])j++;
        if(j==M)return 1;
    }
    return 0;
}

但是显然,这个算法并不太优秀。(可以被卡到\(O(N\cdot M)\))
(尽管对于大多数题够用了)
引入KMP算法。

P1(定义)

对于A=ababababcB=ababc的例子,我们观察一下Check函数的过程。

第一步,两个字符串可以配对到abab(绿边),直到遇到ac,发现不能配对(红边)。此处贡献4步。

第二步,两个字符串再次配对到abab,并再次不能配对,贡献4步。

第三步,两个字符串完全配对,出解,并再次贡献4步。

注:以上贡献次数指除开了遍历了一次A串的步数。



考虑对以上过程优化。
可以发现,我们在第一步失配时,完全可以直接跳到下图的情况。

即我们的j的下标完全可以从4跳到2(失配前的下标)。

即我们如果能够处理出关于B串的一个数组,使得我们可以进行刚才的跳跃操作就好了。

于是乎,我们定义一个 \(Next\) 数组:
\(Next[i]\)表示的是满足如下条件的一个串的末端下标。
该串是\(B[1]\)~\(B[i]\)这个子串的后缀,也是该串的某个前缀,并且是满足以上两个条件的串中长度最长的串。
(限制\(Next[i]\)值不能为\(i\),即跳跃操作至少往前跳1格)。

(语言表达不好,不如来看看例子)

还是之前那个例子:(B=ababc

那么在处理\(Next[4]\)时(即子串abab时),我们暴力的过程如下:
找出该子串的所有后缀(不算自己):

然后找出其所有前缀(同样不算自己):

发现ab为两次查找均出现过串中最长的串,故满足要求,即\(Next[4]\)的值就为\(2\)。

对于该例子(B=ababc)的所有\(Next\)值就是:

失配时跳跃情况如下:

P2(初始化)

那么我们应该如何求出\(Next\)数组呢?

考虑求解\(Next[i]\)时(假设\(Next[1]\)~\(Next[i-1]\)都求好了),我们如何用之前的状态转移。设之前的状态长这样:紫点与绿点是完全相等的两个子串,弧线表示\(Next[i-1]\),我们现在要求红点的\(Next\)值。

那么我们只需要去比较一下下图的红点与橙点是否相等就行了:

如果相等,那么 \(Next[i]\) 就等于 \(Next[i-1]+1\) 。
否则,我们就去访问下标为 \(Next[Next[i]]+1\) 的点再次比较,直到不能比较为止。

因为这样的话,同样也满足\(Next\)数组的性质:
\(B[i-1]=B[Next[i-1]]=B[Next[Next[i-1]]]=...=B[Next[....]]\)
而最终求得的那个前缀,同样会是\(B[1]\)~\(B[i]\)的某个后缀。

P3(求解)

基于P1与P2的内容,P3就比较好理解了。
我们在最初那个暴力Check上改改就行了。
如果失配了,回到一个满足条件的Next[j]就行了。
原理呢,其实和P2初始化部分是一样的。

实在不懂的话,那还是举个例子吧。
对于A=ababababcB=ababc时,我们用KMP算法来做一下。

发现ac失配,现在\(j=4\),考虑让\(j=Next[j]\),更改后\(j=2\),贡献为4。

继续往后,发现失配,现在\(j=4\),再次让\(j=Next[j]\),更改后\(j=2\),贡献为3。

情况变化成下图:

最后出解,贡献为3。

虽然只比暴力的总贡献少两步,但在某些恶意卡暴力的题中,还是得用KMP算法。

代码

板题

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=1000005;
int K,N,M,Next[MAXN],Ans;
char A[MAXN],B[MAXN];
void Prepare(){
    for(int i=2;i<=M;i++){//注意从2开始.
        int j=Next[i-1];
        if(B[j+1]!=B[i]&&j>0)j=Next[j];
        if(B[i]==B[j+1])j++;//判等于0的情况.
        Next[i]=j;
    }
}
int Find(){
    int i=1,j=0;
    for(int i=1,j=0;i<=N;i++){
        while(j&&A[i]!=B[j+1])j=Next[j];
        if(A[i]==B[j+1])j++;
        if(j==M){
            Ans++;
            j=Next[j];
        }
    }
}
int main(){
    scanf("%d",&K);
    while(K--){
        scanf("%s%s",B+1,A+1);
        N=strlen(A+1);M=strlen(B+1);
        Ans=0;Prepare();Find();
        printf("%d\n",Ans);
    }
}
/*
ababababc
ababc
*/

后记

关于KMP算法的时间复杂度:

Q:求解\(Next\)时,\(j\)指针难道不会跳很多次吗,这个复杂度难道不是\(O(M^2)\)吗?

A:其实可以发现,每次j=Next[j]的操作都会使当前的\(Next\)值比上一次的\(Next\)值小。
画出\(Next\)的函数图像如下:

那么对于满足\(Next[i]>Next[i-1]\)的\(Next[i]\)肯定都是在\(O(1)\)的时间里出解的。

而那些\(Next[i]<Next[i-1]\)的\(Next[i]\)总跳跃次数并不会超过\(M\)。
因为\(Next\)函数的值域是在\(M\)以内的,而且一次上升的差距肯定为1。
(即若\(Next[i]>Next[i-1]\),那么有\(Next[i]=Next[i-1]+1\))
而一次回跳至少跳1格,故总回跳次数是不会超过\(M\)次的。



Q:求解时每次失配,\(j\)指针最多还是会跳\(M\)次嘛,看似还是可以卡到\(O(N\cdot M)\)嘛。

A:实则不然,指针\(j\)每次都是和\(i\)一起变化的,只有在\(i\)加1时,\(j\)才有可能跟着加1。这样的话,\(j\)失配往回跳的总次数是不会超过\(N\)次的(每次跳都至少跳1格)。故KMP算法的时间复杂度是十分优秀的\(O(N)+O(M)=O(N+M)\)啦。

原文地址:https://www.cnblogs.com/ftotl/p/11823048.html

时间: 2024-10-10 22:22:57

关于KMP算法的理解的相关文章

KMP算法详细理解

KMP算法详细理解 从昨天开始看KMP算法到今天凌晨..... 把一些知识点进行总结,其实KMP还是挺简单的(HHHHHH) 博客新地址:https://miraitowa2.top/ 1:BF(暴力匹配)算法 假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢? 如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有: 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符: 如

KMP算法的理解

---恢复内容开始--- 在看数据结构的串的讲解的时候,讲到了KMP算法——一个经典的字符串匹配的算法,具体背景自行百度之,是一个很牛的图灵奖得主和他的学生提出的. 一开始看算法的时候很困惑,但是算法思想很简单,就是在暴力匹配的基础上得出的. 暴力匹配 这里有必要说一下暴力匹配,暴力匹配更简单,就是按照人的常规思维去匹配字符串,拿模式串(P)的第一个字符去和给定串(S)比较,S从左往右看,一看,第一个,呀~不对,啥也不说了,第一个都不对了,后边还比个毛.所以,这一次比较,S中第一个字符开头是匹配

KMP算法 --- 深入理解next数组

KMP算法的前缀next数组最通俗的解释 我们在一个母字符串中查找一个子字符串有很多方法.KMP是一种最常见的改进算法,它可以在匹配过程中失配的情况下,有效地多往后面跳几个字符,加快匹配速度. 当然我们可以看到这个算法针对的是子串有对称属性,如果有对称属性,那么就需要向前查找是否有可以再次匹配的内容. 在KMP算法中有个数组,叫做前缀数组,也有的叫next数组,每一个子串有一个固定的next数组,它记录着字符串匹配过程中失配情况下可以向前多跳几个字符,当然它描述的也是子串的对称程度,程度越高,值

KMP算法的理解和代码实现

KMP算法理解参考原文:http://kb.cnblogs.com/page/176818/ 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? 1. 首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较.因为B与A不匹配,所以搜索词后移一位. 2. 因为B与A不匹配,搜索词再往后

KMP算法初步理解

看了两天KMP算法,不知道理解的对不,初步理解总结如下:(主要是各种next数组把自己整晕了,有彻底懂的大神们再给指导下) 首先是思路,"字符串匹配的KMP算法_知识库_博客园"http://kb.cnblogs.com/page/176818/,问题的关键落在求数组上,而求数组实际是对自身求匹配度,所以求next数组的子函数和主函数很类似,所以网上讨论的好像主要是两种next数组,最好把相应的主函数列出来,还有像第二种的next和nextval数组都可用,在主函数相同的情况下,弥补一

字符串匹配KMP算法的理解(详细)

1. 引言 本KMP原文最初写于2年多前的2011年12月,因当时初次接触KMP,思路混乱导致写也写得混乱.所以一直想找机会重新写下KMP,但苦于一直以来对KMP的理解始终不够,故才迟迟没有修改本文. 然近期因开了个算法班,班上专门讲解数据结构.面试.算法,才再次仔细回顾了这个KMP,在综合了一些网友的理解.以及算法班的两位讲师朋友曹博.邹博的理解之后,写了9张PPT,发在微博上.随后,一不做二不休,索性将PPT上的内容整理到了本文之中(后来文章越写越完整,所含内容早已不再是九张PPT 那样简单

KMP算法的理解,伪代码,c代码实现

1.字符串问题形式化定义:假设文本是一个长度为n的T[1..n],而模式是一个长度为m的数组P[1..m],其中m<=n,如果有T[s+1..s+m]==P[1..m],那么就称模式P在T中出现.s为有效偏移,否则称为无效偏移. 2.方法:首先基于模式进行预处理,然后找到所有有效偏移(匹配). 几种方法的预处理时间和匹配时间 算法 预处理时间 匹配时间 朴素算法 0 o((n-m+1)*m) 有限自动机 o(m|所有有限长度字符串的集合|) o(n) KMP o(m) o(n) Rabin-ka

学习笔记之对KMP算法的理解

一,问题引入.    有两个字符串,s[0...n-1] 和 t[0...m-1] 在字符串s中找字符串t:二,思考.    对于这个问题可以逐一比较,即: s s[p] s[p+1] s[p+2] ... s[i-1] s[i] ... ... s[n-1] t t[0] t[1] t[2] ... t[j-1] t[j] ... t[m-1]   即 s[p...i-1] == t[0...j-1],而且 s[i] != t[j], 这个时候最自然的想法是把 t 串右移一位从 t[0] 比较

KMP算法自我理解 和 模板

字符串   abcd abc abcd abc 匹配串   cdabcd 匹配串的 next  0 0 0 0 1 2: 开始匹配 abcd abc abcd abc cd abc d a,d 匹配失败 next 数组进行移动 abcd abc abcd abcd c dabcd 再次匹配 模板 #include<bits/stdc++.h> using namespace std; const int maxn=1e5+10; int nt[1000]; // next 数组首位为 0 in