KMP字符串匹配(看猫片)

KMP 算法介绍

简介

KMP是由三个人的首字母命名的(自己查),大体先说一下,方便有个基本的概念

用途

这里引出一个小问题

给出主串 \(S\) 和 子串 \(a\),求出子串在主串中的数量

最暴力的想法就是哈希表或固定区间挨个判断,类似公交换乘。可是复杂度可想而知,因此有神人发明了 \(KMP\),简单说就是字符匹配

开始我也是觉着还不如哈希表,真绕,但是听完后感觉真香

请忽略作者的调皮

思想

这列给出主串

\[ABCABCAB \]

我们发现没有必要一个一个的移动,当子串为 \({\color{Aquamarine}{A}}\) \({\color{GreenYellow}{B}}\) \({\color{Aquamarine}{CAB}}\) 时,假若与主串匹配到第一个 \({\color{GreenYellow}{B}}\) 就失败了,那么匹配串(主串与子串正在匹配的串,下同)可以往后移动一位,移两位都不会成功(因为移动后得到的匹配串的第一位都不是 \(A\))。

那么往后移\({\color{Magenta}{三}}\)位呢,我们不难发现,第一位必定是 \(A\)

那我们是不是可以省去这次单个比较啦,同理 第二位的 \(B\) 也不用比较了(看上方图解),但是后面的我们并不知道,因此需要再挨个比较

像这样,如果每次都可找到一些不需要匹配的字母,是不是复杂度会大大降低,这就是 \({\color{Purple}{KMP}}\) 算法的思想

\(next\)

上面的想法对应到代码中有个响亮的名字叫失配数组,用 \(next[]\) 表示.具体的说,当子串中的位置 \(i\) 与 主串中位置 \(j\) 匹配失败时,应当用主串中位置 \(j\) 上表示字符在子串中位置 \(k\),这样往后比会更加的省时。

\(next[i]\) 的含义是子串到 \(i\) 位置的前缀串 \(l\) 中最长相同前后缀(不含自身)的前缀结束的位置。

就拿上面的子串 \((ABCAB)\) 来说, 则\(next={-1,-1,-1,0,1}\) \(-1\)表示没有合法的前缀串 \(l\) ,作者一般喜欢设成 \(-1\).

\({\color{GreenYellow}{绿色}}\) 的是已经一样的,\({\color{Magenta}{紫色}}\) 是正在比较的,若判断当前位置 \(i\) 是否与 图前部分紫色位置 \(k\) 一样,只需要知道 \(k\) 往前的前缀串中前缀位置 \(+1\) 是否是 \(k\) 即可,(因为已经一样的子串即绿色部分我们也是按上述进行的操作),因此我们对于 \(k\) 往前的 \(next\) 是已知的,以此类推,就类似 \(dp\) 后面的 \(next\) 由是前面的位置,不理解可以去先看看线性筛素数的优化讲解,用到的\(pre[]\) 的方法和 \(next\) 雷同

//next代码
inline void getnext(){
    next_[0] = -1; 
    int j;
    for(int i = 1;i < ss; ++i)
    {
        j = next_[i - 1];
        while(c[i] != c[j + 1] && j >= 0) 
        j = next_[j];
        next_[i] = c[i] == c[j+1] ? j + 1 : -1;
    }
}

匹配的过程

相互匹配中,主串\(s[]\)的位置为 \(j\), 子串\(ss[]\)的位置为 \(i\) 匹配过程中存在三种情况

  • s[j] == ss[i] 直接后移一位
  • 如果上述不成立,那就早 \(ss[i]\)\(next[i-1]\) 的下一个位置即next[i-1]+1(因为这种情况是在第一条不成立的情况下进行,也就是说前面的是匹配好的,因此调用 \(next\) 了解他的下一个位置是否一样即可)
  • i==0 表示没有合法的\(next\) 可以与 \(j\) 相匹配,那么这个 \(j\)就不法匹配成功, 直接进行下一位的比较
//KMP代码
inline void KMP(){
    int i = 0, j = 0;
    while(j < s && i < ss)
    {
        if(a[j] == c[i])
        {
            i++, j++;//情况一
        }
        else if(i == 0) j++;//情况三
        else i = next_[i - 1] + 1;//情况二
        if(i == ss)//匹配成功啦
        {
            ans++;//记录答案
            i = next_[i-1] + 1;//重置i,(貌似-1也可以)
        }
    }
}

时间复杂度

在整个匹配过程中,主串指针 \(j\) 只会向后走,子串指针 \(i\) 失配是会 \(O(1)\) 到失配位置,因此时间复杂度为

\[ O(n+m) \]

例题

【模板】KMP字符串匹配

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cmath>
#include <cstring>
#define F(i,l,r) for(int i=l;i<=r;i++)
#define FK(x) memset(x,0,sizeof(0))
using namespace std;
/*-----------------------------*/
const int manx = 1e6+10;
const int mod = 1e9+9;
const int base = 1e7+7;

inline int read(){
    char c = getchar();int x = 0,f = 1;
    for(;!isdigit(c);c = getchar())if(c == '-') f = -1;
    for(;isdigit(c);c = getchar()) x = x*10 + (c^48);
    return x * f;
}
/*----------------------------------------------*/ 

int s, ss, n, sum[manx], k, next_[manx];
char c[manx], a[manx];
/*----------------------------------------------*/
inline void prepare(){
    FK(next_);
}
inline void getnext(){
    next_[0] = -1; 
    int j;
    for(int i = 1;i < ss; ++i)
    {
       
        j = next_[i - 1];
        while(c[i] != c[j + 1] && j >= 0) 
        j = next_[j];
        next_[i] = c[i] == c[j+1] ? j + 1 : j;
    }
}
inline void KMP(){
    int i = 0, j = 0, ans = 0;
    while(j < s && i < ss)
    {
        if(a[j] == c[i])
        {
            i++, j++;
        }
        else if(i == 0) j++;
        else i = next_[i - 1] + 1;
        if(i == ss)
        {
            cout<<j-ss+1<<endl;
            i = next_[i - 1] + 1;    
        }
    }
//    return ans;
}
int main(){
        cin >> a ;
        cin >> c ;
        s = strlen(a ), ss = strlen(c );
        getnext();
        KMP();
        F(i,0,ss - 1) cout<< next_[i] + 1 <<" ";
}

:本 \(blog\) 只供了解,毕竟我是一名小菜鸡,写文章时差点把自己绕进去~

作者@_Thorzy,转载请标明出处

posted @ 2021-01-13 21:02  zxsoul  阅读(76)  评论(0编辑  收藏  举报