KMP算法

本文深入讲解KMP算法的工作原理及实现细节,包括next数组的计算方法,并通过多个实例展示其在字符串匹配问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

设主串为"S1S2S3S4.......Sn",模式串为:"P1P2P3.....Pn";难点是当主串中的第i个字符与模式中的第j个字符不匹配的时候,主串中的第i个字符应该与模式中的那个字符再比较,假设已经匹配的样子是:       S1,S2,...,Si-j,Si-j+1.............Si-1,Si.....Sn(绿色匹配,红色不匹配)

                                                          P1,P2....................Pj-1,Pj .........

假设此时应该与第k个字符(k<j)继续相比较,即假设Si与Pk相匹配,更新j的值:如图:

S1,S2,...,Si-j,Si-j+1.....Si-k+1,Si-k+2,.............Si-1, Si.....Sn(绿色匹配,红色不匹配)

                                    P1,        P2....................Pk-1,Pk .....Pj-1,Pj....

则模式串中第k-1个字符的子串一定满足:"P1 P2 P3 .... .Pk-1"="Si-k+1 Si-k+2 ....... Si-1";而已经得到的"部分匹配"结果是:"Pj-k+1 Pj-k+2 .... .Pj-1"="Si-k+1 Si-k+2 .....Si-1";(这个由第一个图可知,不懂的认真分析一下),由上两式知:"P1 P2 P3 .... Pk-1"="Pj-k+1 Pj-k+2 .... Pj-1".

首先的得先分析下KMP算法:当在匹配过程中产生失配时,i不变,j退回到next[j]所指示的位置上重新进行比较,并且当指针j退到0时,指针i和j需同时+1,即若主串中的第i个字符和模式串中的第1个字符不等,应该从主串的第i+1个字符起重新进行匹配。

next[j]表示当模式中的第j个字符与主串中的相应字符"失配"时,在模式中需要重新和主串中该字符进行比较的字符的位置,设为next[j]=k,就是相当于上面的k,现在的主要的内容就是求出next[j]了,首先:next[1]=0,分析过程简略的写一下:

因为:"P1 P2 P3 .... Pk-1"="Pj-k+1 Pj-k+2 .... Pj-1".

①、假设Pk=Pj,所以有::"P1 P2 P3 .... Pk-1 Pk"="Pj-k+1 Pj-k+2 .... Pj-1 Pj".则有:next[j+1]=k+1,next[j+1]=next[j]+1;

②、假设Pk!=Pj,说明::"P1 P2 P3 .... Pk-1 Pk"!="Pj-k+1 Pj-k+2 .... Pj-1 Pj",此时可将求next函数值看成是个模式匹配的问题,整个模式串即是主串又是模式串,模仿KMP算法,只是这个时候就只有Pk!=Pj,说明k位置和j位置不匹配,仿KMP算法,KMP中j应该退回到next[j]位置,这里k应该退回到next[k]的位置,假设:next[k]=k',假设Pj=Pk',则说明在主串中的第j+1个字符之前存在一个长度为k'(即next[k])的最长子串,和模式串中从首字母气长度为k'的子串相等。即:

                                            "P1 P2....Pk'"="Pj-k'+1 Pj-k'+2........Pj"    

这个时候又与第一种情况一样了,所以有next[j+1]=k'+1,即:next[j+1]=next[k]+1,同理若Pj!=Pk',则将模式继续向右滑动直至将模式中的第next[k']个字符和Pj对齐.......依次类推,直至Pj和模式中的某个字符匹配成功或者不存在任何k'满足等式,则next[j+1]=1........认真观察下get_next和KMP算法,是不是非常的相似?

代码为:

class KMP{
public:
    void get_next()//求next[]的值~~
    {   CLR(next,0);//下面的代码中有提到这个意思,对next数组清零
        int i=-1,j=0;
        next[0]=-1;//相当于next[j]=i;
        while(j<s2.length())
        {  if(i==-1||s2[i]==s2[j])//这个很好理解,这个就是上面的Pi=Pj的情况,next[j+1]=k+1,next[j+1]=next[j]+1=i+1,即:next[j]=i;
           {   i++;  j++;  
               next[j]=i;
           }
           else i=next[i];//Pi!=Pj,说明失配,仿KMP算法,i退回到next[i]的位置
       }
    }
    int kmp()
    {   int i=0,j=0,len1=s1.length(),len2=s2.length();
        while(i<len1&&j<len2)//跟while(i<s1.length()&&j<s2.length())有什么区别???????????,为什么用这个不行? 
        {   if(j==-1||s1[i]==s2[j]) {j++;i++;}//继续比较后面的字符
            else j=next[j];   //s1[i]!=s2[j],i位置与j位置失配,则原来j的位置应该变为k位置,而k=next[j],即j=next[j];模式串向右移动,           
        }
        if(j==s2.length()) return i-s2.length();//匹配成功
        else return -1;
    }
    //friend istream& operator>>(istream &,KMP &);  
private:
    string s1,s2; //s1是主串,s2是模式串.  
    int next[MAX];     
};

其实学这个是为了做AC自动机,下面是关于KMP的题目:

nyist 5题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=5是求模式串在主串中出现的最多次数。

#include<iostream>
#include<string>
#include<cstring>
using namespace std;
const int MAX=15;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
class KMP{
public:
    void get_next()
    {   CLR(next,0);
        int i=-1,j=0;
        next[0]=-1;
        while(j<s1.length())
        {   if(i==-1||s1[i]==s1[j])
            {  i++; j++;
               next[j]=i;
            }
            else i=next[i];
        }
    }   
    int kmp()
    {   int i=0,j=0,num=0;
        while(i<s2.length())
        {   if(j==-1||s2[i]==s1[j]) {i++;j++;}
            else j=next[j];
            if(j==s1.length()){ num++;j=next[j];}
        }
        return num;
    }
    friend istream& operator>>(istream &,KMP &);   
private:
    string s1,s2;
    int next[MAX];     
};
istream& operator>>(istream &input,KMP &K)
{   input>>K.s1>>K.s2;
}
int main()
{   int n;
    cin>>n;
    while(n--)
    {  KMP match;
       cin>>match;
       match.get_next();
       cout<<match.kmp()<<endl;
    } 
    return 0;
} 

其实这道题有更简单的方法,利用string中的find函数:

#include<iostream>
#include<cstring>
using namespace std;
string s1,s2;
int main()
{   int sum,n,m;
    cin>>n;
    while(n--)
    {   sum=0;
        string::size_type pos; 
        cin>>s1>>s2;
        pos=s2.find(s1,0);
        while(pos!=string::npos)
        {   sum++;
            pos=s2.find(s1,pos+1); 
        }
        cout<<sum<<endl;
    }
    return 0;
}

和这道题一样的是hdu 1686,只是这个题目要求较高~字符串很长,所以用C++输入输出的方法会TLE,且建议不要使用类~ 
类似的题目还有:hdu 2203(只需要将主串反复一次后进行KMP即可,如:AABCD变成->AABCDAABCD),当然同样可以用string中的find函数。

#include<iostream>
#include<string>
using namespace std;
string s1,s2;
int main()
{   while(cin>>s1>>s2)
    {  s1+=s1;
       if(s1.find(s2)!=string::npos) cout<<"yes"<<endl;
       else cout<<"no"<<endl; 
    }
    return 0;
} 

hdu 1711直接模板即可,给出两个数字串求出模式串在主串中出现的第一个位置。同样得用c语言作为输入输出~~~
hdu 1358,又是一个英语题目,咋那么多的英语题目呢~不说中国的OJ网上的题目中文题多于英文题,最起码也应该差不多吧~~~睡觉睡觉......题目大意:给你一个字符串,问从串头到串尾中的某个位置,字符前缀在字符串中重复了多少次~例:aaa:前缀a,到第二个字符重复了2次,到第三个字符重复了3次~~ 得理解next[]的意义:next[i]代表了前缀和后缀的最大匹配的值。

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=1000010;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,next[MAX],sum=1;
char s[MAX];
void get_next()
{   CLR(next,0);
    int i=-1,j=0;
    next[0]=-1;
    while(j<n)
    {  if(i==-1||s[i]==s[j])
       {   i++;j++;  
           next[j]=i;
       }
       else i=next[i];
    }
}
int main()
{   while(scanf("%d",&n)&&n)
    {  getchar();
       gets(s);
       cout<<"Test case #"<<sum++<<endl;
       get_next();
       for(int i=2;i<=n;i++)
       {  int pos=i-next[i];//求循环节
          if(i%pos==0&&i/pos>1) printf("%d %d\n",i,i/pos);//满足是pos的倍数,且循环次数必须>1
       }
       printf("\n");
    } 
    return 0;
}

hdu 3746,同样是对next[]函数的理解,给你一个字符串要求字符串中的所有字符最少循环两次需要添加的字符是多少?

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int MAX=100010;
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,next[MAX],len;
char s[MAX];
void get_next()
{   CLR(next,0);
    int i=-1,j=0;
    next[0]=-1;
    while(j<len)//好像不可以写成while(j<strlen(s)),会TLE 
    {  if(i==-1||s[i]==s[j])
       {   i++;j++;  
           next[j]=i;
       }
       else i=next[i];
    }
}
int main()
{   scanf("%d",&n);
    getchar();
    while(n--)
    {  gets(s);
       len=strlen(s);
       get_next();
       int sum=len-next[len];//循环节的长度 
       if(len!=sum&&len%sum==0) printf("0\n");//说明所有的字符都相同
       else printf("%d\n",sum-next[len]%sum); 
    } 
    return 0;
}

涉及到KMP的题目总结:POJ 1226,1961,2185,2406,2541,2752,3085,3080,3167,3450,3461。。。HDU 3336,3374,2087等

 


 


 



 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值