KMP 算法

Posted lpf-666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP 算法相关的知识,希望对你有一定的参考价值。

简述

KMP 算法,又称模式匹配算法,能够在线性时间内判定字符串 (A[1-N]) 是否为字符串 (B[1-M]) 的子串。

对于刚刚接触 KMP 的同学来说,理解起来比较困难,难以理解 (next[]) 数组的实际意义。

当然你要硬背 KMP 也没人拦着你,因为代码确实就十几行

但是其实并没有那么难懂,产生畏惧情绪更是不必要的,现在谈谈 KMP。

Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”。

这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

主要的难点和中心点就是 (next[]) 数组和 (f[]) 数组的求解,其中又以 (next[]) 为重。

下文将展开详细的叙述。

暴力算法与整体框架

暴力

首先,暴力很好想,就是枚举比较,时间 (O(NM)) 。 (这个应该都会吧

但是,谁都会发现这种算法的效率很低,但原因是什么,请看下图。

其中 (txt) 是原字符串,(pat) 是子串。

很明显,pat 中根本没有字符 c,根本没必要回退指针 i,暴力解法明显多做了很多不必要的操作。

KMP 框架

KMP 算法的不同之处在于,它会花费空间来记录一些信息,在上述情况中就会显得很聪明:

再比如类似的 (txt = "aaaaaaab") (pat = "aaab")

暴力解法还会和上面那个例子一样蠢蠢地回退指针 i,而 KMP 算法又会耍聪明:

综上,KMP 快速的原因是:避免了不必要的多余匹配

而且,KMP 算法不仅能更高效、更准确的处理这个问题,还可以提供一些额外的信息

具体的讲,KMP 算法共分为两步:

  1. 对字符串 (A) 进行自我匹配,求出一个 (next[]) ,其中 (test[i]) 表示 "(A) 中以 (i) 结尾的非前缀子串" 与 "(A) 的前缀"的最大匹配长度(即最大前缀后缀),即:(next[i]=max){(j)},其中 (j<i) 并且 (A[i-j+1)~(i]=A[1-j])
  2. 对字符串 (A)(B) 进行匹配,求出一个数组 (f),其中 (f[i]) 示 "(B) 中以 (i) 结尾的非前缀子串" 与 "(A) 的前缀"的最大匹配长度,即:(f[i]=max){(j)},其中 (j<i) 并且 (B[i-j+1)~(i]=A[1-j])

是不是发现两者很相像(所以代码也很像),下面我们将详细讲解 (next[]) 数组的求解方法。

next 数组

最长前缀后缀

可能很多同学看到上面两个步骤就昏倒,其实也没有那么难,通俗的讲 (next) 就是最长前缀后缀(好像没有很通俗)

如果给定的模式串 (A=“ABCDABD”),从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表格所示:

技术图片

也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为(下简称《最大长度表》):

技术图片

引理

根据这个表可以得出下述结论:

  • 失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

记住这是很重要的一个结论,证明比较繁琐,可以自行百度,但我是直接记住的。(只在记不住就靠数学能力吧)

这只是通俗的讲,真正的引理是:

(j_0)(next[i]) 的“候选项”,即 (j_0<i)(A[i-j_0+1)~(i]) = (A[1)~(i]) ,则小于 (j_0) 的最大的 (next[i]) 的“候选项”是 (next[j_0]) 。换句话说,(next[j_0]+1)~(j_0-1) 之间的数都不是 (next[i]) 的“候选项”。

请读者务必确定理解这条引理之后再往下看,实在不行可以借助百度以及其他资料

根据引理求解 next 数组

简单说就是根据递归求法求解答案。

假设已求出 (next[i-1]) 的值,则 (next[i-1]) 的所有候选项为 (next[i-1]),(next[next[i-1]]),(next[next[next[i-1]]])等。

(j)(next[i]) 的候选项的前提是 (j-1)(next[i-1]) 的候选项。

(A[i-j+1)~(i]) = (A[1)~(j]) 的前提是 (A[i-j+1)~(i-1]) = (A[1)~(j-1])

因此在计算 (next[i]) 是只需要把 (next[i-1]+1),(next[next[i-1]]+1),(next[next[next[i-1]]]+1)等作为候选项即可。

具体看代码:

  1. 初始化 (next[i]=j=0),假设 (next[i-1]) 已经求出,下面求解 (next[i])
  2. 不断尝试扩张 (j) ,如果扩张失败令 (j=next[j]) ,直至 (j=0)
  3. 如果能够扩展成功(下一个字符相等),匹配长度 (j+1)(next[i]=j)
next[1]=0;
for(int i=2,j=0;i<=n;i++){
    while(j>0 && a[i]!=a[j+1]) j=next[j];
    if(a[i]==a[j+1]) j++;
    next[i]=j;
}

f 数组求解

求解

因为定义的相似性,两者代码几乎一样。

for(int i=1,j=0;i<=m;i++){
    while(j>0 &&(j==n || b[i]!=a[j+1])) j=next[j];
    if(b[i]==a[j+1]) j++;
    f[i]=j;
    //if(f[i]==n) 此时就是 A 在 B 中的某次出现
}

模板题

就是模板题而已。

下面给出完整代码:

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#define maxn 1000010
using namespace std;

int l1,l2,next[maxn],f[maxn];
char s1[maxn],s2[maxn];

int main(){
    cin>>s1+1;
    cin>>s2+1;
    l1=strlen(s1+1);
    l2=strlen(s2+1);
    
    next[1]=0;
    for(int i=2,j=0;i<=l2;i++){
        while(j>0 && s2[i]!=s2[j+1]){
            j=next[j];
        }
        if(s2[j+1]==s2[i]) j++;
        next[i]=j;
    }
    
    for(int i=1,j=0;i<=l1;i++){
        while(j>0 &&(j==l1 || s1[i]!=s2[j+1])){
            j=next[j];
        }
        if(s1[i]==s2[j+1]) j++;
        f[i]=j;
    }
    
    for(int i=1;i<=l1;i++) 
    if(f[i]==l2) printf("%d
",i-l2+1);
    
    for(int i=1;i<=l2;i++)
    printf("%d ",next[i]);
    return 0;
} 

总结

KMP 算法到这里就结束了,这篇文章可能对初学者不太友好,不过也可以加深理解吧。

其时间复杂度为:(O(N+M))

当然你,其复杂度是可以优化的,而且也有更优的算法,时候回继续 (Update)

注:

  1. 部分图片和讲解来自这篇文章
  2. 部分讲解和我的 KMP 入门来自这篇文章
  3. 绝大部分思想来自《算法竞赛进阶指南》。

全篇完。

以上是关于KMP 算法的主要内容,如果未能解决你的问题,请参考以下文章

数据结构—串KMP模式匹配算法

Python ---- KMP(博文推荐+代码)

KMP算法及Python代码

KMP算法及Python代码

图解KMP算法原理及其代码分析

Kmp算法Java代码实现