KMP算法主要用于解决单模式串的匹配问题,即:给定主串s和模式串p,问p是否是s的子串(len(s)<=N, len(p)<=M)。
先考虑最朴素的算法,即枚举s中的起点i,检查s[i..i+M-1]是否等于p,这样的时间复杂度为O(NM)。
分析一下为什么这样的算法效率低(建议读者手动画个图):设指针i和j分别指向s和p中的字符,不妨假定s[0..k-1]和p[0..k-1]已经匹配上了,而s[k]!=p[k](k<M),这说明s[0..M-1]已经不能匹配上p了,根据朴素算法,指针i将移动到s[1],指针j将移动到p[0]重新开始匹配,之后每次失配时主串中的指针i都要回到前面,这就产生了不必要的麻烦。但是如果指针i不回溯而是停在s[k]处,就会漏掉s[l..l+M-1]和p[0..M-1](0<l<k)匹配上的情形。那么为了实现指针i不回溯,我们来重点考察这种情形:假定存在这样的l(0<l<k)使得s[l..l+M-1]和p[0..M-1]匹配上了,那么有s[l..k-1]=p[0..k-l-1],由于之前已经匹配上的部分说明了s[l..k-1]=p[l..k-1],于是推出p[0..k-l-1]=p[l..k-1],也就是说这样的l会使得p[0..k-1]的长度为k-l的前缀和后缀完全相同!如果令next[k]=k-l的话,那么一旦在p[k]处失配,指针i不需要回溯,而指针j只需要指向模式串的next[k]处继续与主串比较就可以了!这就是KMP算法思想的出发点。
那么接下来我们要解决的,就是如何计算next数组。设next[k]=r,根据上一段的分析我们知道r是使得p[0..k-1]的前缀和后缀完全相同的最大长度(之所以选择最大长度是因为,当再次失配的时候可以继续把指针j向前移,就会移到较小的使前后缀完全相同的长度上)。考虑利用递推的方式来求next数组,假定我们已知next[i]=j,现在求next[i+1]:
■■■■■■■■□___________■■■■■■■■□
0 j=next[i] i
如果p[i]=p[j],那么自然有next[i+1]=j+1;如果p[i]!=p[j],那么从定义出发,next[i+1]是最大长度r(r<=j)使得p[0..r-1]=p[i-r+1..i],由next[i]的性质可以推出p[i-r+1..i]=p[i-r+1..i-1]+p[i]=p[j-r+1..j-1]+p[i],所以如果p[i]=p[r-1],那么r就满足p[0..r-2]=p[j-r+1..j-1],这表明next[j]=r-1,即next[i+1]=next[j]+1,此时相当于在比较p[i]和p[next[j]];如果仍然有p[i]!=p[r-1],那么仿照上面的推导过程,只需要比较p[i]和p[next[next[j]]],形成这样一个递归过程:不断令j=next[j],直到最后p[i]等于p[j]或者j=-1为止(如果令next[0]=-1的话),然后令next[++i]=++j就可以了。下面是对模式串p计算next数组的代码:
void CountNext(char p[]) { int i = 0, j = -1; next[0] = -1; while (i != lenp) if (j == -1 || p[i] == p[j]) next[++i] = ++j; else j = next[j]; }
有了next数组以后就可以实现第一段中陈述的匹配过程了,用指针i指向主串s,指针j指向模式串p,如果s[i]=p[j]或者j=-1,那么两个指针各向后移动一位,如果s[i]!=p[j],那么就将j向前移动到next[j]处,即令j=next[j]。当j=len(p)的时候,匹配就成功了,而当i=len(s)的时候,匹配就失败了。下面是利用next数组进行单模式串匹配的代码:
void KMP(char s[], char p[]) { int i = 0, j = 0; while (i != lens) { if (j == -1 || s[i] == p[j]) ++i, ++j; else j = next[j]; if (j == lenp) printf("One matched!\n"), j = next[j]; } }
可以看出两段代码具有很高的相似度。上面这段代码中,将完成一次匹配看作是在p[len(p)]处失配,这样就可以继续查找主串后面的部分与模式串的匹配情况了。
整个KMP算法的时间复杂度为O(N+M),效率非常高。
练习题列表:
POJ3461 - Oulipo
Solutions
POJ3461 - Oulipo
题意:求模式串p在主串s中出现的次数(len(p)<=10,000, len(s)<=1,000,000)。
KMP模板题,每次匹配成功计数器自增1即可。
1 // Problem: poj3461 - Oulipo 2 // Category: KMP Algorithm 3 // Author: Niwatori 4 // Date: 2016/07/23 5 6 #include <stdio.h> 7 #include <string.h> 8 9 int next[10010]; 10 int lens, lenp; 11 12 void CountNext(char p[]) 13 { 14 int i = 0, j = -1; 15 next[0] = -1; 16 while (i != lenp) 17 if (j == -1 || p[i] == p[j]) 18 next[++i] = ++j; 19 else j = next[j]; 20 } 21 22 int KMP(char s[], char p[]) 23 { 24 int i = 0, j = 0, cnt = 0; 25 while (i != lens) 26 { 27 if (j == -1 || s[i] == p[j]) 28 ++i, ++j; 29 else j = next[j]; 30 31 if (j == lenp) 32 ++cnt, j = next[j]; 33 } 34 return cnt; 35 } 36 37 int main() 38 { 39 int t; scanf("%d", &t); 40 while (t--) 41 { 42 char p[10010], s[1000010]; 43 scanf("%s%s", p, s); 44 lenp = strlen(p); 45 lens = strlen(s); 46 CountNext(p); 47 printf("%d\n", KMP(s, p)); 48 } 49 return 0; 50 } 51 52 void KMP(char s[], char p[], int n) 53 { 54 int i = 0, j = 0; 55 while (i != lens) 56 { 57 if (j == -1 || s[i] == p[j]) 58 ++i, ++j; 59 else j = next[j]; 60 61 if (j == lenp) 62 printf("One matched!\n"), j = next[j]; 63 } 64 }