码迷,mamicode.com
首页 > 编程语言 > 详细

KMP 算法

时间:2020-03-09 13:48:16      阅读:73      评论:0      收藏:0      [点我收藏+]

标签:复杂度   tail   模式匹配   友好   pre   blog   目录   信息   例子   

简述

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\n",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 算法

标签:复杂度   tail   模式匹配   友好   pre   blog   目录   信息   例子   

原文地址:https://www.cnblogs.com/lpf-666/p/12448119.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!