这两天在看KMP算法,也搜了各种解释和博客、视频来看。
发现似乎大家的实现方式都不太一样,而且大多没讲到关键点上。
(我现在还不是很懂啊哈哈哈哈哈哈哈哈哈尬笑)
【经典算法】——KMP,深入讲解next数组的求解
先来看道题
HDOJ Problem-1686
求文本T中单词W的出现次数
AC代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
char w[10005],t[1000005];
int nex[10005];
void getnext(char *B,int n)
{
nex[0]=0;
for(int i=1,j=0;i<n;i++)/* n为B串长度 */
{
// j=nex[i-1];j为待计算位前一位对应的最大公共串的下一位(或前一位对应的最大公共串长度)
while(B[j]!=B[i]&&j>0)//若匹配不上,尝试缩小公共子串,因为B串i之前的j个字符(B[i-j]~B[i-1])与开头的j个(B[0]~B[j-1])相同,直接在前面j个字符里找
j=nex[j-1];//找最大公共子串的下一位与B[i]相同的最大公共子串,不能用j--,保证B[0]到B[i-1]这段目前匹配前缀后缀始终相同
if(B[j]==B[i])//公共串下一位字符相同,公共最大长+1
j++;
nex[i]=j;//j为0~i最大公共串长度,若最终B[j]!=B[i],j=0
}
}
int kmp(int m,int n)//m为A串长度,n为B串长度
{
int ans=0,cmp=0;
for(int i=0;i<m;i++){
while(cmp>0&&t[i]!=w[cmp])//第cmp个位置匹配失败
cmp=nex[cmp-1];//下一轮从匹配成功串的最大公共串的下一位开始匹配
if(t[i]==w[cmp])//当前字符匹配成功,继续下一位
cmp++;
if(cmp==n){//子串匹配成功
ans++;
cmp=nex[cmp-1];//本题母串分割各部分间可以有公共部分
}
}
return ans;
}
int main()
{
int n;
cin>>n;
getchar();
while(n--){
gets(w);
gets(t);
getnext(w,strlen(w));
printf("%d\n",kmp(strlen(t),strlen(w)));
}
return 0;
}
求next数组的疑点解释
一个难点就是求next数组
void getnext(char *B,int n)
{
nex[0]=0;
for(int i=1,j=0;i<n;i++)/* n为B串长度 */
{
// j=nex[i-1];j为待计算位前一位对应的最大公共串的下一位(或前一位对应的最大公共串长度)
while(B[j]!=B[i]&&j>0)//若匹配不上,尝试缩小公共子串,因为B串i之前的j个字符(B[i-j]~B[i-1])与开头的j个(B[0]~B[j-1])相同,直接在前面j个字符里找
j=nex[j-1];//找最大公共子串的下一位与B[i]相同的最大公共子串,不能用j--,保证B[0]到B[i-1]这段目前匹配前缀后缀始终相同
if(B[j]==B[i])//公共串下一位字符相同,公共最大长+1
j++;
nex[i]=j;//j为0~i最大公共串长度,若最终B[j]!=B[i],j=0
}
}
next数组存储的数值为截止到该下标的子串的最大公共子串的长度,因为字符串从0开始,该数值也为该子串最大公共子串的前缀部分的下一个字符对应的下标。
用递推的方法来求str对应的next数组,求next[i]时,next[0]~next[i-1]都已经求出,令j为next[i-1]即str[i-1]对应的最大公共串的下一位。
注意:i之前的j个字符(str[i-j]~ str[i-1])与开头的j个(str[0]~str[j-1])相同
①如果str[j]==str[i]的话,说明尾部加入的字符使得上一轮最大公共子串向后延了一位,next[i]=j+1(此时j=next[i-1])
②若str[j]!=str[i],说明加入的字符不能接在上一轮最大公共子串后面,尝试缩小公共子串,所以不断获取最大公共子串的最大公共子串,直至最大公共子串前缀串后面的字符与str[i]匹配上或j==0(str[i]这一段没有最大公共子串),那么就退出循环
注意:不能使用j - -
while(B[j]!=B[i]&&j>0)
j=nex[j-1];//不能换成j--
KMP算法最浅显理解——一看就明白
这篇博客里提到了,但并没有说为什么。
因为使用j - -求出的不保证是str[0]到str[i-1]这一段字符串的公共子串,而是str[0]到str[i-1]这一段字符串的前缀后缀最大公共部分的前XX个字符,新加入的str[i]字符不可以接在这一段后面。
其他疑问
KMP的原理正确性?
如何更好的理解和掌握 KMP 算法? - 咸鱼白的回答 - 知乎
这篇回答为什么要求new数组?
KMP的其他优化形式
扩展KMP算法
拓展kmp算法总结
扩展 KMP 算法
这篇配图很清晰,容易理解
辅助数组next[i]表示字串T[i,m-1]和T的最长公共前缀长度
extend[i]表示T与S[i,n-1]的最长公共前缀
板子题
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
char s[1000005];
int nex[1000005],extend[1000005];
void GetNext(char T[],int m,int nex[])//先计算模式串的next数组
{//之前匹配过程中所达到的最远位置为p(匹配串最后一个字符为T[p-1]),并且以T[a]为起始
int a=0,p=0;//边界情况,此时不能令p=m
nex[0]=m;//与自身完全匹配
//T[i]串和T[i-a]串对齐,来求nex[i]
for(int i=1;i<m;i++)//计算子串T[i]...T[m-1]的匹配长
{
if(i>=p||i+nex[i-a]>=p)//最右匹配串需要更新
{
if(i>=p)
p=i;//i<p时T[i]~T[p-1]与T[0]~T[p-i-1]相同
while(p<m&&T[p]==T[p-i])
p++;//更新最右值,T[p]与T[p-i]匹配失败则跳出
nex[i]=p-i;//最大匹配长T[0]~T[p-i-1]
a=i;//更新最右匹配串起始位置
}
else//在之前的最长匹配串中,所以T[i]==T[i-a],T[i+nex[i-a]]==T[i-a+nex[i-a]],且T[i-a]串在T[nex[i-a]]处截断(相当于T[0+nex[i-a]]!=T[i-a+nex[i-a]])
nex[i]=nex[i-a];//所以T[i]也在T[nex[i-a]]处截断(T[i+nex[i-a]]!=T[0+nex[i-a]]),即为匹配串长度
}
}
void GetExtend(char S[],int n,char T[],int m,int extend[],int next[])
{//注意:比较过程中T[0]始终与S[a]对齐
int a=0,p=0;//a,p为母串S上的标记,p为之前匹配过程中所达到的最远位置
GetNext(T,m,next);
for(int i=0;i<n;i++)//计算子串S[i]...S[n-1]与T的匹配长
{//i一定≥a,所以要求p尽量在最右,而不必特别关注a
if(i>=p||i+next[i-a]>=p)//a和p都要更新
{
if(i>=p)//i>=p的作用:举个典型例子,S和T无一字符相同
p=i;//i>=p时更新最右值p,i<p时S[i]~S[p-1]与T[0]~T[p-i-1]匹配
while(p<n&&p-i<m&&S[p]==T[p-i])//
p++;
extend[i]=p-i;//S[i]~S[p-1],共p-i个字符匹配
a=i;//更新最右匹配串起始位置,当i超出了p时,以i开头重新计算p
}//但当p<i&&i+next[i-a]>=p时,a更不更新不影响
else//未超出最右匹配串,即S[i]~S[i+nex[i-a]]==T[i-a]~T[i-a+nex[i-a]],T[0+nex[i-a]]!=T[i-a+nex[i-a]]
extend[i]=next[i-a];//所以S[i+nex[i-a]]!=T[0+nex[i-a]],nex[i-a]即为匹配串长度
}
}
int main()
{
int t;
cin>>t;
while(t--){
scanf("%s",s);
int len=strlen(s);
long long cnt=0;//果然是卡int...
memset(nex,0,sizeof(nex));
// memset(extend,0,sizeof(extend));
GetNext(s,len,nex);
// GetExtend(s,len,s,len,extend,nex);
// {
// cout << "next: ";
// for (int i = 0; i < len; i++)
// cout << nex[i] << " ";
//
// // 打印 extend
// cout << "\nextend: ";
// for (int i = 0; i < len; i++)
// cout << extend[i] << " ";
//
// cout << endl << endl;
// }
for(int i=1;i<=len-1;i++){
cnt+=nex[i];
if(i+nex[i]<len)
cnt++;
}
// for(int i=1;i<=len-1;i++){
// cnt+=extend[i];
// if(i+extend[i]<len)
// cnt++;
// }
// printf("cnt=%d,cnt2=%d\n",cnt,cnt2);
// cnt++;//最后一个字符
cout<<cnt<<endl;
}
return 0;
}