ROS本来在上个寒假的时间就简单学习了一下KMP但是当时也没有看的很明白再加上好久没接触OI了,前段时间要用KMP的时候都有点忘了
最近ROS终于把KMP弄懂了!
在此浅谈一下KMP
(ROS是在这篇洛谷题解中看明白KMP的——>传送门)
ROS是针对洛谷P3375来讲解KMP的,但是看本篇解析完全可以不需要看原题。(看一下更好)
话说KMP居然是普及组算法.......我还看了好长时间来着
如果想要官方好好正式地学习KMP,ROS还是会建议你去看上面那篇我推荐的题解。ROS在此只是用通俗的方式来浅谈KMP。(以及我在学习KMP时遇到的一些难点)
ROS这篇题解会写的非常的通俗易懂但是又有点思路杂乱无章,还望各位大佬看后莫喷
首先,什么是KMP?
KMP是一个算法
上百度百科:
光看百科的解释你可能不会十分了解这个算法,那么此时就需要一个情景来帮助你理解KMP算法
一道字符串匹配的题目:给你一个文本串A和一个模式串B(已知长度:A>B),现在需要你在文本串A中找到完整连续不间断的B在A中的位置。
直接看到这道题,如果让你自己用肉眼来看的话自然是在文本串A中来对比每个字符然后找到和B的第一个字符相同的字符之后从这个字符往后找lb(B字符的长度)个字符分别与B的每个字符比较,如果不是的话就往后找。然后再找和模式串B中的第一个字符相同的位置,以此类推。
那么如果把这种思想给写成代码的话那么很明显这是一个属于O(mn)的算法,是一个很慢的算法。如果用这种算法的话就好像用dfs来写dp的题........(在后面均称此算法为原始算法)
所以KMP便应运而生了!
KMP的核心是什么呢?
这句话是KMP整个算法的核心,如果你看这句话就看懂了的话说明你已经掌握了KMP了!
能直接看懂这句话的,十个里面九个学过,还有一个是大佬
这句话乍一看可能十分难懂所以ROS要重点解释一下
就是给一个样例,文本串A:ABCABBCBABBCBAACAAB;模式串B:ABBCBAA
(A的长度是19,B的长度是7)
KMP和我们说的原始算法在此还有一大区别便是,我们在原始算法中是将文本串和模式串均向后移动,我们在任何操作中都是将文本串和模式串同时向后移动的。然而在KMP算法中,我们在匹配失败后的操作更倾向于移动模式串而并非从头匹配。
如上面的样例我们再进行匹配时从文本串的第四位到第九位均与模式串的第一位到第六位相同。然而文本串的第十位与模式串的第七位不同。原始算法中,我们会从文本串的第五位开始与模式串的第一位进行比较直到又遇到一位不同的字符或者匹配成功之后,在文本串中我们相当于一位位进行匹配。因此时间复杂度会很慢很慢
那么KMP算法就是在第四位到第九位都与模式串的第一位到第六位相同,但是第十位和模式串的第七位不同时,模式串会自动移动,而文本串并不会回到第五位进行匹配。
在此处详细来说就是:当匹配过程中发现文本串的第十位和模式串的第七位不同时,模式串会自动移动,将模式串中最长的相同前后缀进行移动。使得原先位于后缀位置的文本串的模式串在移动后变成模式串的最长前缀位置。(此时需要明确一点,就是此处的前后缀必须是真前后缀,而不能是整个模式串本身,否则在此处的移动中是没有意义的)即模式串中第6位的最长相同前后缀为“A”,那么位于模式串第六位的A将会由模式串中第一位的A替代,之后文本串的第十位将会与模式串的第二位进行匹配。是不是快了许多!
解析:我们首先用一个next数组来储存模式串中从第一位到达这一位中的最长相同前后缀的长度(注意在c++11中不要使用next来做数组名,否则会报错)。
并且特殊规定next[1]=0(实际上无需特殊规定,因为next[1]并无实际用处)
如对于模式串B来说:
next[1]==0,next[2]==0,next[3]==0,next[4]==0,next[5]==0,next[6]==1,next[7]==1
那么我们在上述问题中匹配到了第六位,而第七位不符合。因为next[6]==1,所以我们便将模式串的第1位与文本串最后匹配的那一位,然后再接着匹配就好了。
正确性证明:由于我们对于第i位有此处的最长相同前后缀长度为next[i],那么对于我们来说其实只需要前next[i]位就可以了因为在从文本串最后一个匹配位向前数next[i]位开始作为模式串开始匹配的位置。而由于next[i]是模式串第i位的最长相同前后缀的长度,所以说一定有符合从第i位向前next[i]位置分别与模式串的next[i]位相同。而与前next[i]位没有匹配的位数此时还没有匹配,所以说从后往前数next[i]位之前的字符一定不可能符合条件。
正确性简略证明完毕。
那么怎么来进行匹配呢?
我们此处可以写一个solve函数来解决问题:
1 void solve(){ 2 int j=0; 3 for(int i=1;i<=la;i++){ 4 while(j&&a[i]!=b[j+1]) j=next[j]; 5 if(a[i]==b[j+1]) j++; 6 if(j==lb){ 7 printf("%d ",i-lb+1); 8 j=next[j]; 9 } 10 } 11 for(int i=1;i<=lb;i++){ 12 printf("%d ",next[i]); 13 } 14 return ; 15 }
整体思路与分析的思路都差不多,只不过是代码化了而已。弄懂了还是很好理解的。
那么next函数怎么生成呢?只需要模式串与自身匹配一遍就可以了!如果匹配成功的话就将j+1的数字存储在next数组里
1 void kmp(){ 2 int j=0; 3 for(int i=2;i<=lb;i++){ 4 while(j&&b[i]!=b[j+1]) j=next[j]; 5 if(b[i]==b[j+1]) j++; 6 next[i]=j; 7 } 8 return ; 9 }
最后上P3375的总代码:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #define N 1000010 5 using namespace std; 6 char a[N],b[N]; //a是文本串,b是模式串 7 int next[N],la,lb; 8 void kmp(){ 9 int j=0; 10 for(int i=2;i<=lb;i++){ 11 while(j&&b[i]!=b[j+1]) j=next[j]; 12 if(b[i]==b[j+1]) j++; 13 next[i]=j; 14 } 15 return ; 16 } 17 void solve(){ 18 int j=0; 19 for(int i=1;i<=la;i++){ 20 while(j&&a[i]!=b[j+1]) j=next[j]; 21 if(a[i]==b[j+1]) j++; 22 if(j==lb){ 23 printf("%d ",i-lb+1); 24 j=next[j]; 25 } 26 } 27 for(int i=1;i<=lb;i++){ 28 printf("%d ",next[i]); 29 } 30 return ; 31 } 32 int main(){ 33 scanf("%s%s",a+1,b+1); 34 la=strlen(a+1);lb=strlen(b+1); 35 kmp(); 36 solve(); 37 return 0; 38 }