目录
1.KMP算法是什么?
kmp算法是一种用于高效进行字符串匹配的算法,KMP的核心就是next数组的构造与使用.
2.KMP算法有什么用?
一般用于解决这种问题:
涉及字符串匹配的问题.
标志性经典问题:
现有一字符串a,一字符串b,问a串中是否包含b串,更甚至问包含多少次b串.
解决这种字符串匹配的问题看似不难,可是如果字符串太长,比如a串有一百万长度,b串长度一万,因最简单的暴力匹配复杂度最坏为n*m,极易爆掉,故而需要优化,而KMP算法的最坏复杂度为n+m,比暴力匹配好太多了
3.KMP实现思路
(1)暴力匹配为何低效
既然暴力匹配低效,那么必然是暴力匹配做了无意义的事,那么我们先来看看暴力匹配做了什么无意义的事.
假设a串为1231231234,b串为1234,我们对a做暴力匹配
for(int i=0;a[i];i++){
int j=0;
while(b[j]&&a[i+j]==b[j]) j++;
if(!b[j]) printf("在%d位置匹配成功",i);
}
外层i=0循环时,内层循环123其实都已经匹配成功,可是此时第四个4匹配失败,a中这个位置是1,故而我们只好放弃此次配对,将i++,继续开始下一次匹配,可是明明我们看得出来,b串是1开头,23其实根本不符合,如果我们分析过b串,也许能知道在第四个匹配失败时直接i+3,这不就节约了效率吗.
其实上面那组ab不是暴力匹配下最坏的情况,最坏的是a串为1111111111111111111111111112,b串为11111112,这种情况下暴力匹配的复杂度会达到最坏的n*m,按着暴力匹配的程序想一遍你会发现你对a串中的第七个1比较了7次,为什么会这样?因为你在b中最后那个2匹配失败时,明知道这前面的7个1是已经匹配成功的,却没有去使用这个条件.
总结暴力匹配低效的教训,就是匹配失败时一定要充分利用已经被匹配成功的这一部分,也许你并不需要回归起点重新开始!
(2)kmp怎么去利用匹配成功的部分完成优化的
首先介绍KMP的一个重要的小细节,kmp算法a串中每个位置只读一次,至于怎么为什么暴力匹配会读多次因为如果暴力读不每次都回到i+1的地方重新匹配是怕出现下面这种情况缺失:
a为123123124,b为123124,虽然会在a[5]处失配,可如果直接在a[5]处重新开始与b[0]匹配,显然错了,为什么?因为a[5]失配只是从a[0]开始匹配的结果,也许以比如a[3]开始,会成功匹配,故而暴力匹配只能憋屈的在a[5]失配时回到a[1]与b[0]重新开始匹配,可到这里我们也许会发现一个小细节,在a[5]失配时,已匹配串12312与b串123124,12312的后缀12与12312的前缀12相同!为什么强调这里后面就知道了.
现在开始介绍KMP算法实现思路:就是在匹配失败时,通过分析b串与已匹配成功的串的最长相同前后缀,来尽可能少的倒退b串的已匹配位置j.next[j]意为b中第j位置前b串的最长相同前后缀长度(真实还要减1,因为数组下标从0开始,next[j]意义其实是在i位置匹配失败则将j跳转到next[j]),当j==-1自然表示前方已匹配串没有可用的后缀,故而继续看后面的,直到某个位置b串全部匹配,则匹配成功.
(3)kmp过程
比如上面的a串1111111111111111111111111112,b串11111112,在匹配2失败时,我们发现b的前缀与已成功的部分的后缀最长居然7位为相同,故而b串只用以b[6]继续匹配.如下图
由b串11111112构造出的next数组:
1 1 1 1 1 1 1 2
next[]:-1 0 1 2 3 4 5 6
1111111111111111111111111112
11111112 此时i=7,j=7 失配 j=next[j]
11111112 此时i=7,j=6 成功 i++,j++
11111112 此时i=8,j=7 失配 j=next[j]
11111112 此时i=8,j=6 成功 i++,j++
11111112 此时i=9,j=7 失配 j=next[j]
思路就是上面这样子了.
4.模版代码
这个模版中kmp函数用于输出a中每个匹配b串的下标,其实kmp算法的核心只是next数组的构造,在失配时学会跟着next跳转就好.
#include <stdio.h>
const int maxn=100005;
int next[maxn];//next数组
void getnext(char* s){//构造next数组,真正的模版
next[0]=-1;
int i=0,j=-1; //j为什么初值赋值-1?0其实也行,仅仅是为了少一个判断,
while(s[i]){
if(j==-1||s[i]==s[j]) next[++i]=++j;
else j=next[j];
}
}
void kmp(char* a,char* b){ //输出a中每个匹配b串的下标,不同问题这个函数的写法多变
int blen=0;
while(b[blen]) blen++;
getnext(b);
int i=0,j=0;
while(a[i]){
if(j==-1||a[i]==b[j]){
i++,j++;
if(!b[j]){
printf("%d ",i-blen);
j=next[j];
}
}
else j=next[j];
}
}
int main(){
kmp("abcabcabc","abcab");
return 0;
}
5.例题与源码
1.HDOJ 2087 剪花布条(模版题)
Problem Description
一块花布条,里面有些图案,另有一块直接可用的小饰条,里面也有一些图案。对于给定的花布条和小饰条,计算一下能从花布条中尽可能剪出几块小饰条来呢?
Input
输入中含有一些数据,分别是成对出现的花布条和小饰条,其布条都是用可见ASCII字符表示的,可见的ASCII字符有多少个,布条的花纹也有多少种花样。花纹条和小饰条不会超过1000个字符长。如果遇见#字符,则不再进行工作。
Output
输出能从花纹布中剪出的最多小饰条个数,如果一块都没有,那就老老实实输出0,每个结果之间应换行。
Sample Input
abcde a3aaaaaa aa#
Sample Output
03
题目分析:
就是求a串中的b串个数,不过这里要注意因为是剪布条,自然不能重叠,故而我的模版代码的kmp函数需要你自己做一些修改.
当然,一道1000数据的题你用暴力也能过,不过别忘了,你为什么看到这里.
ac代码:
#include <stdio.h>
const int maxn=100005;
int next[maxn];//next数组
void getnext(char* s){//构造next数组,真正的模版
next[0]=-1;
int i=0,j=-1; //j为什么初值赋值-1?0其实也行,仅仅是为了少一个判断,
while(s[i]){
if(j==-1||s[i]==s[j]) next[++i]=++j;
else j=next[j];
}
}
int kmp(char* a,char* b){
int ansnum=0; //最开始答案为0
getnext(b);
int i=0,j=0;
while(a[i]){
if(j==-1||a[i]==b[j]){
i++,j++;
if(!b[j]){
ansnum++; //有一个满足的就加1
j=0; //j直接彻底重置,避免重叠
}
}
else j=next[j];
}
return ansnum; //返回答案
}
char a[10000],b[10000];
void solve(){
while(1){
scanf("%s",&a);
if(a[0]=='#'&&!a[1]) break; //判断输入结束
scanf("%s",&b);
printf("%d\n",kmp(a,b));
}
}
int main(){
solve();
return 0;
}
2.待加入高难度KMP题