讲KM♂P算法之前,我们先讲一个故♂事。
有一天,sgg给了老obo一封信和一个单词,并给他一个任务:找出这封信出现了多少个单词,然后在规定时间内告诉他。
碰到这个问题,老obo会怎么做呢?
首先最直观的想法是什么?
就是先一维循环枚举信字符串的开头,再一维循环枚举单词的长度,一个一个判断是不是相同。这样的时间复杂度是O(n*m)的,n是信的长度,m是单词的长度。
当n*m小的时候,可以在规定时间内出解,然而,要是n*m很大呢?
我们想想有没有能优化以上算法的方法
我们假设一个next数组,next[i]代表1~i部分的字符串,最长的前缀等于后缀是多长,注意,1~i字符串的长度不能作为next[i]存储的答案。
这样有什么用呢?考虑这样:
我们考虑对单词做出这个数组:next={0,0,0,1,0,0,1,2,0}(从0位置开始)
来模拟一下我们一开始匹配的过程
设定一个指针j,初始在0
我们令i在信中从1扫描到8
:1位置,a[i]==b[j+1],我们令j++
2位置,a[i]==b[j+1],j++
3位置,a[i]==b[j+1],j++
4位置,a[i]==b[j+1],j++
5位置,a[i]==b[j+1],j++
6位置,a[i]==b[j+1],j++
7位置,a[i]==b[j+1],j++
8位置,a[i]!=b[j+1],那怎么办呢,如果让j直接等于0就相当于功亏一篑,于是我们让j=next[j],这样发现,之前匹配的s没有浪费掉,而且节约了时间。然后j=2,我们验证一下,在b中12位置的"sb"正好等于a中67位置的"sb"!
9位置,a[i]==b[j+1],j++
.........
直到11位置,j==m (m为单词长度),这表明了匹配成功!
那么我们怎么求这个next数组呢?
先给出模板
void kmp(){
next[]=;int j=;
for (int i=;i<=m;i++){
while (j>&&b[j+]!=b[i]) j=next[j];
if (b[j+]==b[i]) j++;
next[i]=j;
}
}
就是这么简♂短,首先,next[1]=0,毋庸置疑,我们之前有声明过,next[i]不能等于1~i的长度。
然后对于后面的i,我们已知了next[i-1]的值,我们先让j=next[i-1],
如果b[j+1]=b[i],说明加上这个字符b[i],就等同于在原来相同的前缀后缀都增加了b[i]这个字符。
如果不相同,那就让j=next[j],这个部分比较难理解,我们画张♂图
假设这个字符串是单词,此时我的i已经到了9,j此时是4,当第10个字符出现的时候,如果它"失配"了,那么j=next[j],
其中我们知道,next[4]=2,我们看一下,长度为2的"sx"也正好是这个字符串的前缀和后缀,这是因为,如果我当前的next值为4,那么说明我前缀后缀都包含了"sxsx"这个字符串,当我再度让j=next[j],也就是j从4变成2的时候,"sx"也刚好是他们的前缀后缀了!
这样,O(m)计算next数组的方法就出来了。
obo:那怎么和信匹配呢?
给个模板:
void match(){
int j=;
for (int i=;i<=n;i++){
while (j>&&b[j+]!=a[i]) j=next[j];
if (b[j+]==a[i]) j++;
if (j==m){
printf("Yes!");
return;
}
}
}
是不是很眼熟?没错,和处理next的程序很像,这是因为,kmp中处理next的过程正是"自我匹配"的过程,因为如果a能和b匹配,那a中也包含了b,所以匹配的过程本质是一样的。