KMP算法详解.docx
《KMP算法详解.docx》由会员分享,可在线阅读,更多相关《KMP算法详解.docx(17页珍藏版)》请在冰豆网上搜索。
KMP算法详解
KMP算法详解(C++版)
KMP算法是一种字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法).
KMP 算法之所以难懂,很大一部分原因是很多实现的方法在一些细节的差异。
然后去看另外的方法,就全都乱了!
体现在几个方面:
next 数组,有的叫做“失配函数”,其实是一个东西; next 数组中,有的是以下标为 0 开始的,有的是以 1 开始的; KMP 主算法中,当发生失配时,取的 next数组的值也不一样!
就这样,各说各的,乱的很!
所以,在阐述我的理解之前,我有必要说明一下,我是用 next 数组的, next 数组是以下标 0 开始的!
还有,我不会在一些基础的概念上浪费太多,所以你在看这篇文章时必须要懂得一些基本的概念,例如 “ 朴素字符串匹配 ”“ 前缀 ” , “ 后缀 ” 等!
假设在我们的匹配过程中出现了这一种情况:
根据 KMP 算法,在该失配位会调用该位的 next 数组的值!
在这里有必要来说一下 next 数组的作用!
说的太繁琐怕你听不懂,让我用一句话来说明:
返回失配位之前的最长公共前后缀!
好,不管你懂不懂这句话,我下面的文字和图应该会让你懂这句话的意思以及作用的!
首先,我们取之前已经匹配的部分(即蓝色的那部分!
)
我们在上面说到 next 数组的作用时,说到 “ 最长公共前后缀 ” ,体现到图中就是这个样子!
接下来,就是最重要的了!
没错,这个就是 next 数组的作用了 :
返回当前的最长公共前后缀长度,假设为 len 。
因为数组是由 0 开始的,所以 next 数组让第 len位与主串匹配就是拿最长前缀之后的第 1 位与失配位重新匹配,避免匹配串从头开始!
如下图所示!
(重新匹配刚才的失配位!
)
如果都说成这样你都不明白,那么你真的得重新理解什么是 KMP 算法了!
接下来最重要的,也是 KMP 算法的核心所在,就是 next 数组的求解!
不过,在这里我找到了一个全新的理解方法!
如果你懂的上面我写的的,那么下面的内容你只需稍微思考一下就行了!
跟刚才一样,我用一句话来阐述一下 next 数组的求解方法,其实也就是两个字:
继承
a 、当前面字符的前一个字符的对称程度为 0 的时候,只要将当前字符与子串第一个字符进行比较。
这个很好理解啊,前面都是 0 ,说明都不对称了,如果多加了一个字符,要对称的话最多是当前的和第一个对称。
比如 agcta 这个里面 t 的是 0 ,那么后面的 a 的对称程度只需要看它是不是等于第一个字符 a 了。
b 、按照这个推理,我们就可以总结一个规律,不仅前面是 0 呀,如果前面一个字符的 next 值是 1 ,那么我们就把当前字符与子串第二个字符进行比较,因为前面的是 1 ,说明前面的字符已经和第一个相等了,如果这个又与第二个相等了,说明对称程度就是 2 了。
有两个字符对称了。
比如上面 agctag ,倒数第二个 a 的 next 是 1 ,说明它和第一个 a 对称了,接着我们就把最后一个 g 与第二个 g 比较,又相等,自然对称程度就累加了,就是 2 了。
c 、按照上面的推理,如果一直相等,就一直累加,可以一直推啊,推到这里应该一点难度都没有吧,如果你觉得有难度说明我写的太失败了。
当然不可能会那么顺利让我们一直对称下去,如果遇到下一个不相等了,那么说明不能继承前面的对称性了,这种情况只能说明没有那么多对称了,但是不能说明一点对称性都没有,所以遇到这种情况就要重新来考虑,这个也是难点所在。
如果蓝色的部分相同,则当前 next 数组的值为上一个 next 的值加一,如果不相同,就是我们下面要说的!
如果不相同,用一句话来说,就是:
从前面来找子前后缀
1 、如果要存在对称性,那么对称程度肯定比前面这个的对称程度小,所以要找个更小的对称,这个不用解释了吧,如果大那么就继承前面的对称性了。
2 、要找更小的对称,必然在对称内部还存在子对称,而且这个必须紧接着在子对称之后。
如果看不懂,那么看一下图吧!
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:
"前缀"和"后缀"。
"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。
以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A,AB],后缀为[BC,C],共有元素的长度0;
- "ABCD"的前缀为[A,AB,ABC],后缀为[BCD,CD,D],共有元素的长度为0;
- "ABCDA"的前缀为[A,AB,ABC,ABCD],后缀为[BCDA,CDA,DA,A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A,AB,ABC,ABCD,ABCDA],后缀为[BCDAB,CDAB,DAB,AB,B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A,AB,ABC,ABCD,ABCDA,ABCDAB],后缀为[BCDABD,CDABD,DABD,ABD,BD,D],共有元素的长度为0。
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。
比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。
搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
2.next数组的求解思路
通过上文完全可以对kmp算法的原理有个清晰的了解,那么下一步就是编程实现了,其中最重要的就是如何根据待匹配的模版字符串求出对应每一位的最大相同前后缀的长度。
我先给出我的代码:
voidmakeNext(constcharP[],intnext[])
{
intq,k;//q:
模版字符串下标;k:
最大前后缀长度
intm=strlen(P);//模版字符串长度
next[0]=0;//模版字符串的第一个字符的最大前后缀长度为0
for(q=1,k=0;q{
while(k>0&&P[q]!
=P[k])//递归的求出P[0]···P[q]的最大的相同的前后缀长度k
k=next[k-1];//不理解没关系看下面的分析,这个while循环是整段代码的精髓所在,确实不好理解(abcxabfsabcxabc)
if(P[q]==P[k])//如果相等,那么最大相同前后缀长度加1
{
k++;
}
next[q]=k;
}
}
现在我着重讲解一下while循环所做的工作:
1. 已知前一步计算时最大相同的前后缀长度为k(k>0),即P[0]···P[k-1];
2. 此时比较第k项P[k]与P[q],如图1所示
3. 如果P[K]等于P[q],那么很简单跳出while循环;
4. 关键!
关键有木有!
关键如果不等呢?
?
?
那么我们应该利用已经得到的next[0]···next[k-1]来求P[0]···P[k-1]这个子串中最大相同前后缀,可能有同学要问了——为什么要求P[0]···P[k-1]的最大相同前后缀呢?
?
?
是啊!
为什么呢?
原因在于P[k]已经和P[q]失配了,而且P[q-k]··· P[q-1]又与P[0]···P[k-1]相同,看来P[0]···P[k-1]这么长的子串是用不了了,那么我要找个同样也是P[0]打头、P[k-1]结尾的子串即P[0]···P[j-1](j==next[k-1]),看看它的下一项P[j]是否能和P[q]匹配。
如图2所示
附代码:
#include
#include
voidmakeNext(constcharP[],intnext[])
{
intq,k;
intm=strlen(P);
next[0]=0;
for(q=1,k=0;q{
while(k>0&&P[q]!
=P[k])
k=next[k-1];
if(P[q]==P[k])
{
k++;
}
next[q]=k;
}
}
intkmp(constcharT[],constcharP[],intnext[])
{
intn,m;
inti,q;
n=strlen(T);
m=strlen(P);
makeNext(P,next);
for(i=0,q=0;i{
while(q>0&&P[q]!
=T[i])
q=next[q-1];
if(P[q]==T[i])
{
q++;
}
if(q==m)
{
printf("Patternoccurswithshift:
%d\n",(i-m+1));
}
}
}
intmain()
{
inti;
intnext[20]={0};
charT[100]={0};
charP[100]={0};
scanf("%s%s",&T,&P);
kmp(T,P,next);
for(i=0;i{
printf("%d",next[i]);
}
printf("\n");
return0;
}
poj2406(kmpnext数组)
大意:
给出一个字符串问它最多由多少相同的字串组成
SampleInput
abcd
aaaa
ababab
.
SampleOutput
1
4
3
分析:
kmp中的next数组求最小循环节的应用
#include
#include
#include
usingnamespacestd;
constintmaxn=1000005;
intnext[maxn];
chars[maxn];
voidget(constchars[])
{
intq,k;
intm=strlen(s);
next[0]=0;
for(q=1,k=0;q{
while(k>0&&s[q]!
=s[k])
k=next[k-1];
if(s[q]==s[k])
{
k++;
}
next[q]=k;
}
}
intmain(){
while(gets(s)){
if(s[0]=='.')break;
get(s);
intans=1;
intl=strlen(s);
if(l%(l-next[l-1])==0){
ans=l/(l-next[l-1]);
}
printf("%d\n",ans);
}
}
POJ2752SeektheName,SeektheFame(KMP)
给你一个字符串s,从小到大输出s中既是前缀又是后缀的子串的长度。
利用next数组的存储的性质,即可得出正确的答案。
。
。
#include
#include
#include
#include
usingnamespacestd;
constintN=400005;
intNext[N],ans[N];
chars[N];
voidgetnext(){
intj=0,k=-1;
intl=strlen(s);
Next[0]=-1;
while(jif(k==-1||s[j]==s[k]){
j++;k++;
Next[j]=k;
}
else
k=Next[k];
}
}
voidkmp(){
getnext();
intl=strlen(s);
inti=l,sum=0;
while(i!
=0){
sum++;
ans[sum]=Next[i];
i=Next[i];
}
for(i=sum-1;i>=1;i--)
printf("%d",ans[i]);
printf("%d\n",l);
}
intmain(){
while(~scanf("%s",s)){
kmp();
}
return0;
}
poj2185
题意:
给出一个大矩阵,求最小覆盖矩阵,大矩阵可由这个小矩阵拼成。
(就如同拼磁砖,允许最后有残缺)
正确解法的参考链接:
http:
//poj.org/showmessage?
message_id=153316
在discuss里还看到有人说可以这么简化:
求横向最小长度时每次比较整列
求纵向最小长度时每次比较整行
真的是太神了!
http:
//poj.org/showmessage?
message_id=168710
一开始,我也是按照错误的解法来求得。
也就是用KMP的next求出每行的最小循环子串长度,然后求这些长度的公倍数,作为宽(若大于col,则为col)。
然后用KMP的next求出每列的最小循环子串长度,然后求出这些长度的公倍数,作为长(若大于row,则为row)。
这种解法是过不了下面的样例的:
Input
28
ABCDEFAB
ABCDEABC
28
ABCDEFAB
AAAABAAA
Output
16
12
对于第一个样例,可以这么做。
但对于第二个样例,就不行了。
因为AAAABAAA它的循环子串可以理解为AAAAB,也可以理解为AAAABA,AAAABAA,AAAABAAA
而这里取AAAABA,正好与第一行的ABCDEF同样为6,所以答案为12。
但是这样的解法也可以AC,说明POJ数据比较弱。
#include
#include
#include
#include
#include
#include
usingnamespacestd;
constintmaxn=10000+10;
constintmaxm=80;
charmat[maxn][maxm];
charrevmat[maxm][maxn];
intr,c;
intP[maxn],F[maxn];
voidgetP(){
P[1]=P[0]=0;
for(inti=1;iintj=P[i];
while(j&&strcmp(mat[i],mat[j]))j=P[j];
if(strcmp(mat[i],mat[j])==0)P[i+1]=j+1;
elseP[i+1]=0;
}
}
voidgetF(){
F[1]=F[0]=0;
for(inti=1;iintj=F[i];
while(j&&strcmp(revmat[i],revmat[j]))j=F[j];
if(strcmp(revmat[i],revmat[j])==0)F[i+1]=j+1;
elseF[i+1]=0;
}
}
voidgetRev(){
for(inti=0;ifor(intj=0;jrevmat[i][j]=mat[j][i];
}
}
}
voidsolve(){
intL=r-P[r],R=c-F[c];
printf("%d\n",L*R);
}
intmain(){
while(~scanf("%d%d",&r,&c)){
for(inti=0;igetP();
getRev();
getF();
solve();
}
return0;
}
poj3080BlueJeans
大致题意:
多组数据,每组给定m个字符串,求这些字符串最长公共子串。
若子串长度小于3,则输出nosignificantcommonalities,否则就输出这个最长公共子串。
若有多个最长公共子串(长度相等),则取其中字典序最小的那个。
Input
3
2
GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3
GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATA
GATACTAGATACTAGATACTAGATACTAAAGGAAAGGGAAAAGGGGAAAAAGGGGGAAAA
GATACCAGATACCAGATACCAGATACCAAAGGAAAGGGAAAAGGGGAAAAAGGGGGAAAA
3
CATCATCATCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
ACATCATCATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AACATCATCATTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
Output
nosignificantcommonalities
AGATAC
CATCATCAT
#include
#include
charstr[20][70];
chartmp[70];
intp[70];
voidgetp(intm,char*b1){
charb[70];
strcpy(b+1,b1);
p[1]=0;
inti,j=0;
for(i=2;i<=m;i++){
while(j>0&&b[j+1]!
=b[i])j=p[j];
if(b[j+1]==b[i])j+=1;
p[i]=j;
}
}
boolkmp(char*a1,char*b1,intn,intm)
{
chara[70],b[70];
strcpy(a+1,a1);strcpy(b+1,b1);
inti,j=0,cnt=0;
for(i=1;i<=n;i++){
while(j>0&&b[j+1]!
=a[i])j=p[j];
if(b[j+1]==a[i])j+=1;
if(j==m){
returntrue;
}
}
returnfalse;
}
boolcheck(char*s,inttot)
{
inti,j;
for(i=2;i<=tot;i++)
{
intn=strlen(str[i]+1),m=strlen(s);
if(!
kmp(str[i]+1,s,n,m))
returnfalse;
}
returntrue;
}
charans[70];
intmain()
{
intt,n,i,j;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%s",str[i]+1);
intlen=strlen(str[1]+1);
intL=0;
for(i=1;i<=len;i++)
{
for(j=1;j<=len-i+1;j++)
{
strncpy(tmp,str[1]+j,i);
memset(p,0,sizeof(p));
getp(i,tmp);
if(check(tmp,n))
{
if(i>=L)
{
if(strcmp(tmp,ans)<0&&i==L)strcpy(ans,tmp);
if(i>L)strcpy(ans,tmp);
L=strlen(ans);
}
}
memset(tmp,0,sizeof(tmp));
}
}
if(L>=3)puts(ans);
elseputs("nosignificantcommonalities");
}
}