KMP详解
学了数据结构,突然想起自己KMP还没有整理
1.问题提出
假设我们现在面临这样一个问题:有一个文本串S和一个模式串T,现在要查找P在S中的位置,该如何查找呢?
2.BF(暴力匹配法)
2.1思想
从主串的第一个字符开始和模式T的第一个字符进行比较。若相等,则继续比较两者的后续字符;否则,从主创S的第二个字符开始和模式T的第一个字符进行比较。重复上述过程,直至S或T中所有字符比较完毕。若T中的字符全部比较完毕,则匹配成功,返回本趟匹配的开始位置;否则匹配失效,返回0。
时间复杂度:最好情况时为O(m+n);最坏情况为O(m*n)
伪代码就不写了,直接上代码~
2.2代码
int BF(char S[],char T[]){
int start=0;
int i=0,j=0;
while((S[i]!='\0')&&(T[j]!='\0')){
if(S[i]==T[j]){
i++,j++;
}else{
start++;
i=start;
j=0;
}
}
if(T[j]=='\0')
return start+1;
return 0;
}
3.KMP算法
3.1定义
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法
3.2解释
BF算法中,一旦S和T匹配失败后,T的下标就会回溯,有些回溯是不必要的,因此,希望某趟在S和T匹配是白虎,下标i不回溯,下标j回溯至某个位置k,使得T[k]对准S[i]继续进行比较。显然,关键问题是如何确定位置k。
观察部分匹配成功时的特征,某趟在S[i]和T[j]匹配失败后,下一趟比较从S[i]和T[k]开始,则有T[0] ~ T[k-1] = S[i-k] ~ S[i-1]成立
在部分匹配成功时,有T[j-k] ~ T[j-1] = S[i-k] ~S[i-1]
有上面两个式子结合可知
T[0] ~ T[k-1] = T[j-k] ~ T[j-1]
该式子说明,模式中的每一个字符T[j]都对应一个k值,这个k值仅依赖于模式本身,与主串无关。用next[j]表示T[j]对应的k值(0<=j<m)其定义如下:
next数组解析如下
① 蓝色框之前为abc,没有前缀和后缀相同的字符,所以为0
② 绿色框之前虽然有ab这个字符串是重复的,但是abcaabbc的前缀和后缀是a和c,相等的串里边要包含前缀和后缀
求next数组代码
void getNext(char p[],int next[]){
next[0]=-1;
int i=0,j=-1;
while(i<strlen(p)){
if(j==-1||p[i]==p[j]){
i++;
j++;
next[i]=j;
}
else
j=next[j];
}
}
3.3伪代码
int kmp(char *a,char *b){
int i=0,j=0;
getNext(b);
for(int k=0;k<strlen(b);k++)
cout<<next[k];
int len1=strlen(a),len2=strlen(b);
while(i<len1&&j<len2){
if(j==-1||a[i]==b[j]){
i++;j++;
}
else
j=next[j];
}
if(j==strlen(b))
return i-j;
return -1;
}
这里出现了个小问题:kmp函数中while(i<len1&&j<len2)不能写成while(i<strlen(a)&&j<strlen(b))
strlen返回的是一个size_t类型,size_t类型是unsigned int,unsigned int 和int比较,int会强转成高级的unsigned int。计算机里面使用补码保存数据,所以j = -1时转成unsigned int 0xffffffff
4.经典例题
Number Sequence
此题可水,不贴代码
Oulipo
#include <bits/stdc++.h>
using namespace std;
int next[1000100];
int sum=0;
void getNext(char *ptr){
int i=0,j=-1;
next[0]=-1;
while(i<strlen(ptr)){
if(j==-1||ptr[i]==ptr[j]){
i++;j++;
next[i]=j;
}
else
j=next[j];
}
}
void kmp(char *a,char *b){
int i=0,j=0;
getNext(b);
int len1=strlen(a),len2=strlen(b);
while(i<len1&&j<len2){
if(j==-1||a[i]==b[j]){
i++;j++;
}
else
j=next[j];
if(j==len2){
sum++;
j=next[j];
}
}
}
char a[1000100],b[1000100];
int main(){
int t;
cin>>t;
while(t--){
sum=0;
cin>>b>>a;
memset(next,0,sizeof(next));
kmp(a,b);
cout<<sum<<endl;
}
return 0;
}
Seek the Name, Seek the Fame
题解
给一个字符串S, 求出所有前缀pre,使得这个前缀也正好是S的后缀。 输出所有前缀的结束位置。
例如 “ababcababababcabab”, 以下这些前缀也同时是S的后缀
ab : 位置2
abab : 位置4
ababcabab : 位置9
ababcababababcabab : 位置 18
如图所示,根据next[ ]前缀的性质左边有颜色部分和右边有颜色部分完全相等
因为左边的红色和右边的红色相等,如果左边的红色和左边的绿色相等,则左边的红色必定与右边的绿色相等
既左红+左蓝+左绿和左绿是我们要求的子串
根据这个规律不断地递推下去,所有的子串都可以求出来
代码
#include <bits/stdc++.h>
using namespace std;
int next[400010];
int sum=0;
void getNext(char *ptr){
int i=0,j=-1;
next[0]=-1;
int len=strlen(ptr);//这里一定要记住用len 不然会TLE
while(i<len){
if(j==-1||ptr[i]==ptr[j]){
i++;j++;
next[i]=j;
}
else
j=next[j];
}
}
char a[400010];
int main(){
while(cin>>a){
int newNext[400010];
memset(next,0,sizeof(next));
getNext(a);
int cnt=0;
int len=strlen(a);
newNext[0]=len;
int k=next[len];
while(k>0){
newNext[++cnt]=k;
k=next[k];
}
for(int i=cnt;i>=0;i--){
cout<<newNext[i];
if(i!=0)
cout<<" ";
}
cout<<endl;
}
return 0;
}