首先我们来看这样的一道题目:
有一个文本串s(长的)和一个模式串p(短的),现在要在s的子串中间找到p,怎么找?
第一,肯定会想到暴力(手打):
{
int i;///s中匹配到了i
int j;///p中匹配到了j
int slen=ss.size();
int plen=pp.size();
while(i<slen&&j<plen)
{
if(ss[i]==pp[j])
{
i++;
j++;
}
else///如果没有匹配到,i那么就要回头到ss中开始匹配的下一个字符,j就要回到pp开头
{
i=i-(j-1);
j=0;
}
}
if(j==plen)///pp全部匹配完了(找到了pp),跳出了循环
{
return i-j;
}
else
return -1;
}
但是我们发现这样每次没匹配到时,都会往回退很多,直至开头,那么有没有一种算法能够一直向前呢?
kmp来了。
既然说kmp可以做到不完全回去,那么我们总要跳到一个地方。
所以我们会有一个next数组(就是前面已经匹配了的,只有从这里开始可能匹配到,之前的不可能匹配到)
就是这个模式串中的前缀和后缀最长的重合的部分的字符数。(这里我觉得解释不清楚,所以需要自己慢慢思考)
所以next数组是在确定了模式串之后就已经确定下来的。
next[i]表示从p[0]到p[i-1]这个字符串的next值,即使前面所说的前缀和后缀最长的重合部分的字符数。(求next要去掉当前这一位的)
规定next[0]=-1,next[1]=0;
举个例子:ababaca的next数组next[7]=[-1,0,0,1,2,1,0],就是从next[0]到next[6](-1是指不存在,只有next[0]会是-1)
那么怎么求呢?
下面给出函数:(自己看一下就明白了,把每个变量表示什么弄清楚就明白了)
int next[mm];
void getnext(string p)
{
int plength=p.size();
next[0]=-1;
int k=-1;
int j=0;
while(j<plength-1)//p[k]表示前缀,p[j]表示后缀
{
if(k==-1||p[j]==p[k])
{
k++;
j++;
next[j]=k;
}
else
{
k=next[k];
}
}
}
再就是主要的kmp函数了:(多看几遍,搞清楚变量什么意思就知道了)
void kmp(string s,string p)
{
int slength=s.size();
int plength=p.size();
int i=0;
int j=0;
while(i<slen&&j<plenth)
{
///注意j=-1的情况,j=next[j]可能到-1;
if(j==-1||s[i]==p[j])///如果j==-1或者匹配成功,都是i++,j++;
{
i++;
j++;
}
else
{
j=next[j];
}
}
if(j==plength)
return i-j;
else
return -1;
}
这样最主要的两个函数就完成了。
真正遇到题目时,会有所修改:(以洛谷 p3375为例)
题目如下:
题目描述
如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。
为了减少骗分的情况,接下来还要输出子串的前缀数组next。
(如果你不知道这是什么意思也不要问,去百度搜[kmp算法]学习一下就知道了。)
输入输出格式
输入格式:第一行为一个字符串,即为s1
第二行为一个字符串,即为s2
输出格式:若干行,每行包含一个整数,表示s2在s1中出现的位置
接下来1行,包括length(s2)个整数,表示前缀数组next[i]的值。
输入输出样例
说明
时空限制:1000ms,128M
数据规模:
设s1长度为N,s2长度为M
对于30%的数据:N<=15,M<=5
对于70%的数据:N<=10000,M<=100
对于100%的数据:N<=1000000,M<=1000000
样例说明:
所以两个匹配位置为1和3,输出1、3
kmp模板,我自己手打代码如下:(函数与上面说的略有变动)
///自学kmp
#include<iostream>
using namespace std;
const int mm=1001000;
string sss;
string ppp;
int violent(string ss,string pp)//ss为文本串(长),pp为模式串(短)
{
int i;///s中匹配到了i
int j;///p中匹配到了j
int slen=ss.size();
int plen=pp.size();
while(i<slen&&j<plen)
{
if(ss[i]==pp[j])
{
i++;
j++;
}
else///如果没有匹配到,i那么就要回头到ss中开始匹配的下一个字符,j就要回到pp开头
{
i=i-(j-1);
j=0;
}
}
if(j==plen)///pp全部匹配完了(找到了pp),跳出了循环
{
return i-j;
}
else
return -1;
}
int next[mm];
void getnext(string p)
{
int plength=p.size();
next[0]=-1;
int k=-1;
int j=0;
while(j<plength)//p[k]表示前缀,p[j]表示后缀
{
if(k==-1||p[j]==p[k])
{
k++;
j++;
next[j]=k;
}
else
{
k=next[k];
}
}
}
void kmp(string s,string p)
{
int slength=s.size();
int plength=p.size();
int i=0;
int j=0;
while(i<slength)
{
///注意j=-1的情况,j=next[j]可能到-1;
if(j==-1||s[i]==p[j])///如果j==-1或者匹配成功,都是i++,j++;
{
i++;
j++;
}
else
{
j=next[j];
}if(j==plength)cout<<i-j+1<<endl;
}
}
int main()
{
cin>>sss>>ppp;
getnext(ppp);
kmp(sss,ppp);
for(int i=1;i<=ppp.size();i++)
{
cout<<next[i]<<' ';
}
return 0;
}
初学者的程序,有问题欢迎讨论。