字符串匹配之KMP算法

一.算法引入.

KMP算法是一种字符串匹配算法,用于一个串匹配另一个串的算法.

我们先来介绍几个定义:

称一个串用 A A A去匹配串 B B B,表示 A A A B B B中找到一个子串与 A A A相同,这时我们称 A A A为模式串, B B B为主串.

定义一个字符串 s s s的子串 s [ l . . r ] s[l..r] s[l..r] s s s中从第 l l l个字母到第 r r r个字母组成的子串.

字符串的下标从 1 1 1开始,串 A A A的长度为 n n n,串 B B B的长度为 m m m.


二.朴素算法的实现.

一般的暴力匹配算法只能做到 O ( n m ) O(nm) O(nm)匹配(虽然很少跑满).

这种暴力匹配的算法,就是枚举主串的每一个点,然后暴力判断从这个点开头的子串是否与模式串相同.

这种算法的代码如下:

void work(){
  //b是模式串,仅求匹配次数sum,等会KMP的代码也是一样
  int j;
  for (int i=1;i<=n;++i){
    for (j=1;j<=m&&a[j]==b[i+j-1];++j)
    if (j>m) ++sum;
  }
}

这个算法的实现比较简单,然而效率并不高,因此我们需要优化.


三.KMP算法对暴力的优化.

KMP引入了一个在字符串匹配方面非常著名的优化方案:失配指针.以下我们将失配指针储存在 n e x t next next数组中.

失配指针在KMP中定义为: n e x t [ i ] next[i] next[i]表示一个最大的数满足 n e x t [ i ] < i next[i]next[i]<i s [ 1.. n e x t [ i ] ] = s [ i − n e x t [ i ] + 1 , n e x t [ i ] ] s[1..next[i]]=s[i-next[i]+1,next[i]] s[1..next[i]]=s[inext[i]+1,next[i]],也就是最长的一个既是 s [ 1.. i ] s[1..i] s[1..i]的前缀又是后缀的串的长度.

可以好好感受以下这个定义代表什么意思,看懂这个定义基本就能猜到如何实现KMP算法了.

KMP算法的过程分为两步,第一步为求模式串的 n e x t next next数组(自我匹配),第二步为利用模式串的 n e x t next next数组匹配主串.

那么我们先讨论第一步.考虑让 i i i 1 1 1枚举到 n n n,然后我们考虑若第 i i i个字符与第 n e x t [ i − 1 ] + 1 next[i-1]+1 next[i1]+1个字符是否相同,分两种情况:

1.相同.这时候显然 n e x t [ i ] = n e x t [ i − 1 ] + 1 next[i]=next[i-1]+1 next[i]=next[i1]+1.

2.不相同.这时候代表失配了,但是我们可以尝试让第 i i i个字符与第 n e x t [ n e x t [ i − 1 ] ] + 1 next[next[i-1]]+1 next[next[i1]]+1个字符匹配(考虑 n e x t next next数组的意义,显然这样做是正确的),若还是失配,我们可以继续尝试匹配 n e x t [ n e x t [ n e x t [ i − 1 ] ] ] + 1 next[next[next[i-1]]]+1 next[next[next[i1]]]+1

那么这个自我匹配的过程就很明了了,代码如下:

void self_mate(){
  nxt[1]=0;
  int j=0;
  for (int i=2;i<=m;++i){
    while (b[j+1]^b[i]&&j>0) j=nxt[j];
    if (b[j+1]==b[i]) ++j;
    nxt[i]=j;
  }
}

其实第二步也差不多,我们只需要像上面一样匹配就差不多了,看代码理解吧:

int mate(){
  int j=0,sum=0;
  for (int i=1;i<=n;++i){
    while (b[j+1]^a[i]&&j>0) j=nxt[j];
    if (b[j+1]==a[i]) ++j;
    if (j==m) ++sum,j=nxt[j];
  }
  return sum;
}

这样子就是KMP算法的全部了,虽然我感觉自己写的不全面.


四.时间复杂度分析.

我们接下来以自我匹配部分为例分析一下时间复杂度.

仔细一看自我匹配部分在外层有一个 O ( n ) O(n) O(n)的for循环,内层有一个while循环,显然我们只要分析出while循环的循环总量为 O ( n ) O(n) O(n)的就可以了.

看一下代码,我们发现这个while循环的循环次数只与j有关,因为j增加的地方只有一处而且一次只加 1 1 1,总共加 n n n次,也就是说 j j j最多会加上 n n n.然后看while循环中,根据 n e x t next next的定义,每次执行 j = n e x t [ j ] j=next[j] j=next[j]时, j j j必定减少至少 1 1 1,二且最多减少到 0 0 0,所以 j j j就最多只会减 n n n次,那么while循环总量最多也就是 O ( n ) O(n) O(n).

所以自我匹配时间复杂度为 O ( n ) O(n) O(n).匹配主串时间复杂度类似,为 O ( n + m ) O(n+m) O(n+m).


五.next数组的性质与next树.

n e x t next next数组代表的意义是一个前缀即是前缀又是后缀的最长子串长度,那么它又有什么性质呢?

在KMP的过程中我们已经分析过一点, n e x t [ i ] next[i] next[i]是前缀 S [ 1.. i ] S[1..i] S[1..i]的最长前后缀长度,那么 n e x t [ n e x t [ i ] ] next[next[i]] next[next[i]]是第二长的, n e x t [ n e x t [ n e x t [ i ] ] ] next[next[next[i]]] next[next[next[i]]]是第三长…

考虑把每个位置看成一个点,并在每个位置代表的点与它的 n e x t next next之间连一条边,以位置 1 1 1为根,那么我们会发现这变成了一棵树.

再看上面的哪条性质,我们会发现一个点到根的路径上的节点的所代表的字符串的长度是递减的,而且必然是这个点的一个前缀或后缀.

同样的,在一个点为根的子树上所有节点所代表的字符串都会比这个点长,而且这个点是子树上所有点的前缀或后缀.

事实上,大部分KMP题不用到 n e x t next next树的概念也可以很好的解释,但是当KMP拓展到Trie树上变成了AC自动机后,那么在AC自动机上的 n e x t next next树—— f a i l fail fail树就很有用了.


六.KMP例题与模板.

题目:luogu3375.

代码如下:

#include
  using namespace std;
 
#define Abigail inline void
typedef long long LL;
 
const int N=1000000;
 
char a[N+9],b[N+9];
int nxt[N+9],n,m;
 
int handle(char *a){
  int n=0;
  for (;a[n];++n);
  for (int i=n;i>=0;--i) a[i+1]=a[i];
  return n;
}
 
void self_mate(){
  nxt[1]=0;
  int j=0;
  for (int i=2;i<=m;++i){
    while (b[j+1]^b[i]&&j>0) j=nxt[j];
    if (b[j+1]==b[i]) ++j;
    nxt[i]=j;
  }
}
 
int mate(){
  int j=0,sum=0;
  for (int i=1;i<=n;++i){
    while (b[j+1]^a[i]&&j>0) j=nxt[j];
    if (b[j+1]==a[i]) ++j;
    if (j==m) ++sum,printf("%d\n",i-m+1),j=nxt[j];
  }
  return sum;
}
 
Abigail into(){
  scanf("%s",a); 
  scanf("%s",b);
  n=handle(a);m=handle(b);
}
 
Abigail work(){
  self_mate();
  mate();
}
 
Abigail outo(){
  for (int i=1;i<=m;++i)
    printf("%d ",nxt[i]);
  puts("");
}
 
int main(){
  into();
  work();
  outo();
  return 0;
}

你可能感兴趣的:(算法入门)