[笔记]KMP算法
[笔记]KMP算法
用途,应用的领域
kmp算法主要用于字符串的匹配,即给定一个模式串和一个文本串,让你找出模式串在文本串中出现的位置或次数等等,简而言之就是一种进行字符串比对的优秀算法.
建议先看看文末的VersionII
算法过程&原理
1.首先我们考虑用暴力的方法比配两个字符串,一个是ABCDBK(模式串)另一个是ABCDBC(文本串)这个例子比较特殊(不存在border),但暴力方法还是可以做的,具体做法是让模式串一位一位的跟文本串匹配,一旦在某一位(任意一位),就将模式串的第一位整体向文本串的后一位移动,图示.
2.为了验证kmp的优秀我们再举一组例子,模式串ABAC,文本串ABABAC,我们先按照暴力的方法,匹配出模式串和文本串的前三个字符是一样的,但到了第四个字符就不一样了,此时如果按照暴力我们应将模式串的第一个字符移动到与文本串的第二个字符对齐的位置,但很明显这不优秀.我们可以发现在模式串中第1个字符和第3个字符都是A,此时我们有另一种相较于暴力更优的移动方案,就是将现在模式串的第一位移动到现在模式串第3个字符所在的位置,而第一,第三这两个字符是一样的,因此可以继续匹配,这就是kmp的大致过程.
3.从2.中我们可以感受到kmp算法的关键,即与暴力算法的不同之处就是当面对失配字符时的应对策略,所以我们当前应该解决的就是如何让模式串在失配时进行合理的移动,上文我们的移动规则是:当我们遇到一个失陪字符时,寻找这个失配字符前一个字符在模式串中出现的地方(向前找),并将模式串相应移动.
4.规定一下:我们称失配字符的前一个字符为X字符.同时我们需要注意到,由于我们是将整个模式串一起移动,而且X字符不一定是开头(第一个)字符,所以我们需要保证在进行移动时一定要保证X字符之前的字符也要相应的能找到在开头对应的字符,否则就会出现移动后有错误的情况.
5.解释程序中的一些变量和语句:
1)kmp数组,kmp[i]存储的是以第i个字符结尾(以从kmp[1]
到kmp[i - 1]
)的子串的border的长度,简言之就是说在由前\(i\)个字符组成的子串中最长相同的前缀和后缀的长度.
2)在求解kmp时需要注意,为了避免出现4.中所提到的错误,一定要保证在kmp[i]
前的字符也能找到匹配才能将border长度累加,否则则让当前的字符i作为新的一个子串的开头去找border.
3)在4.和5.的基础上验证一下如果避免了4.中的错误,即移动的均是在前面找到相同的字符的话,为什么这样做是对的.X字符是最后一位已匹配的字符,说明在它之前一定都在文本串中匹配成功,此时再将以X字符为结尾的border移过来,相当于是用一个字符组成完全相同只是下表不一样的字符子串代替了以X字符为结尾的border,对于文本串而言仍是匹配的,因此正确性可以保证.
对着代码讲算法
#include <bits/stdc++.h>
using namespace std;
int kmp[1000010];
int main(){
char a[1000010],b[1000010];
cin>>a + 1;
cin>>b + 1;
int lena = strlen(a + 1),lenb = strlen(b + 1);
int p;
for(int i = 2;i <= lenb;i++){
p = kmp[i - 1];
while(p > 0 && b[i] != b[p + 1])
p = kmp[p];
if(b[i] == b[p + 1])
p++;
kmp[i] = p;
}
p = 0;
for(int i = 1;i <= lena;i++){
while(p > 0 && b[p + 1] != a[i])
p = kmp[p];
if(b[p + 1] == a[i])
p++;
if(p == lenb){
cout<<i - lenb + 1<<endl;
p = kmp[p];//不能直接赋值为1,原因见下
}
}
for(int i = 1;i <= lenb;i++){
cout<<kmp[i]<<" ";
}
cout<<endl;
return 0;
}
补充说明:我们假设有文本串\(ABABABABAB\),模式串\(BABA\)(下标从1开始),第一个在文本串中出现的答案是下标从2~5的子串,当匹配完成后,\(p\)的值为4(因为模式串已经匹配成功,所以下标在最后一位);kmp[] = {0,0,0,1,2}
,kmp[p] = 2;
,这个时候说明只要将模式串往后移动\(2\)位就会有新的答案,而如果我们直接让p=1
,则会使文本串中下标为4~7的答案被忽略.
Version II
再一次学习之后又有了新的理解:
上面讲的有点复杂,这里重新讲一下求\(next(kmp)\)数组.
首先我们需要明确,\(kmp\)或\(next\)数组是指模式串中的最长相同前缀后缀,\(kmp[i]\)表示的是从第\(1\)个字符到第\(i\)个字符组成的子串中最长相同前缀后缀的长度.举个栗子:现有模式串\(ABCAB\),第\(4\)个字符的kmp数组值是\(1\),因为第\(4\)个字符是\(A\),而模式串的第一位也是\(A\);第\(5\)个字符的\(kmp\)数组值是\(2\),因为模式串的前两位和\(4,5\)位是一样的.
现在我们再来分析一下代码
一开始有一个赋值语句p=kmp[i-1]
这句活的意思是在求当前第\(i\)位的\(kmp\)值时,先让它等于它前一位的\(kmp\)值.还是举个栗子,仍然是\(ABCAB\)作为模式串,当我们再求第\(5\)个字符的\(kmp\)值时,先给它赋值为它的前一位\(A\)的\(kmp\)值,这样才能求出正确的数值,否则如果又重新匹配子串\(ABCAB\),这样的结果是第\(5\)位的\(kmp\)值为\(0\)这显然是错的
再看下面的while(b[p+1]!=b[i]&&p>=0)
,首先我们知道\(kmp\)里面存的是一个长度,b[p+1]
可以先想一想是什么,其实就是当前在求值的这一位的前一位的对应的前缀的最后一位的后一位.非常绕口,还是刚才那个例子,模式串\(ABCAB\),现在我们要求第\(5\)位的\(kmp\)值,此时它前一位的\(kmp\)值为\(1\),它前一位对应的前缀就是第一个\(A\),那么b[p+1]
其实就是第一个\(A\)的后一位\(B\);其实到这里我们可以发现,因为\(kmp\)保存的是最长相同前缀后缀的值,所以其实它的值就对应了模式串的一个前缀的长度,第\(i\)个字符就和模式串中的第\(kmp[i]\)个字符是一样的(kmp[i]!=-1
).也就是说\(kmp\)数组标记的是一个\(border\)的最后一位.
后面的就好理解了.