这个算法我从2018-02-11在清北学堂的课上学了第1次,后在大二的数据结构课上学了第2次,然后又到现在学习了第3次,最终觉得自己理解了这个算法
本篇中我们以P代表模板串,就是在题目中通常长度较短的串
我们在S(模式串),也就是较长的串中,寻找P,并记录开始匹配和完成匹配的下标
这个算法的核心思想就是:每次失配后,我们并不是像暴力算法那样只前进一格,而是前进到这样一个位置:设在当前位置下部分匹配的字符串为A,我们将P串移动到仍然和这个A部分匹配的位置
为什么要移动到这个位置呢?
我们可以思考一下,往前移动会出现这样几种情况:
1.一点也不匹配,即P串的第一个位置和S就不匹配,这样的移动无疑是浪费,应该跳过
2.部分匹配,但是匹配的长度小于A,同理,这样的匹配不如匹配到A优
3.部分匹配(匹配长度>A的长度)或者完全匹配,这种情况必然是匹配到A的长度演化而来
综上,我们在一次失配后,移动到可以和本次成功匹配的前缀匹配的位置,这样的移动是正确的
如何确定每一次移动多少才能满足上述的条件呢?我们使用了next数组
next数组的含义是,p串中真前缀和真后缀(即不包括P串自己)相同的最大长度是多少
对于P="abcab",它的next数组为
p | a | b | c | a | b |
i | 1 | 2 | 3 | 4 | 5 |
next[i] | 0 | 0 | 0 | 1 | 2 |
对于i=2,p="ab",前缀为a;后缀为b,两者不相等,next[2]=0
对于i=5,p="abcab",前缀为a,ab,abc,abca;后缀为b,ab,cab,bcab;可以看到最大相等的前缀后缀为ab,长度为2,next[5]=2
next[i]=j的含义就是:p[1...j] = p[i-j+1...j]
关于匹配过程:
这张图介绍了匹配的过程,比如开始我们部分匹配成功的为3串,我们接下来后移时,按照kmp的移位法则,我们需要尽可能向后移且使得1串=2串,也即是4串=2串(1串是由4串平移得到,所以1串=4串) ,这个移动距离我们就要使用next数组里的值了
那么如何求next数组呢?是通过模板串P自己匹配自己实现的,匹配操作几乎一样,设用Pj去匹配Pi,每次i++时,我们记录下此时Pj在Pi中的部分匹配的长度(即j走了多少)即可
kmp代码如下
#include <iostream>
using namespace std;
const int N=1e5+10,M=1e6+10;
char p[N],s[M];
int ne[M];
int main()
{
int n,m;
cin>>n>>p+1>>m>>s+1;
//求next数组
//以下的j应理解为,已经匹配完成的位置
for(int i=2,j=0;i<=n;i++)
{
while(j && p[i]!=p[j+1]) j=ne[j];//如果失配就移位
//判断j是否为0是因为,如果跳到了0的位置就不必再跳了
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
//匹配过程
for(int i=1,j=0;i<=m;i++)
{
while(j && s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n)
{
cout<<i-n<<' ';
j=ne[j];
}
}
return 0;
}