Q:给定两个字符串A、B,求A在B中出现了多少次
这是KMP的经典问题,我们将从这里开始引入KMP算法
KMP算法の思想
KMP算法分为两步
1.对A进行自我匹配,求出A的nxt数组
其中表示 A中以结尾的非前缀子串 与 A的前缀的最大匹配长度
即若,则~ ~
2.将A与B匹配,求出数组
其中表示 B中以结尾的子串 与 A的前缀的最大匹配长度
煮个栗子
那么该如何快速求出nxt数组呢
假设我们已经知道~,现在求
求得过程实际上就是 求一个最大的,满足~~且
然后令
对于这个找得过程,我们可以按照从大到小的顺序
先找到所有满足第一个条件的,再判断第二个条件是否成立
对于,满足第一个条件的最大的显然是
然后我们判断第二个条件成立,所以
在考虑,同样先取最大的满足条件一的
但这时候发现,条件二不成立
于是我们再找第二大的满足条件一,而这个正好就是
但是,条件二依然不成立
再找第三大的满足条件一,这个等于
很可惜条件二还是不成立
再找第四大的满足条件一,到这里前面已经没有字符了
所以直接判断是否有,这里满足了条件,所以
通过上面的 栗子 我们已经对KMP的思路有了基本了解
在这里再一次用稍形式化的语言描述一次
假设当前已求出~
现在需要求一个最大的,满足~~且
令
首先尝试最大的满足条件一的,判断条件二是否成立
若成立则,否则,重复上述步骤
当时直接判断是否有,若是则,否则等于0
算法的总时间复杂度为
KMPの代码实现
由于一般情况下读入字符串时第一个字符会储存在第0号位
为了方便运算我们将nxt[]表示的值都-1,但记得实际上表示的长度应该再+1
void qnxt(char* ss,int len)
{
int j=-1; nxt[0]=-1;
for(int i=1;i<len;++i)
{
while(j!=-1&&ss[i]!=ss[j+1]) j=nxt[j];
if(ss[i]==ss[j+1]) j++;
nxt[i]=j;
}
}
求完数组后在考虑数组
由于其定义的基本相同,所以求解方法已基本一致
注意为了运算方便保存在中的值也是减了1的
void KMP(char* a,char* b)
{
int n=strlen(a),m=strlen(b);
int j=-1; qnxt(a,n);
for(int i=0;i<m;++i)
{
while(j!=-1&&b[i]!=a[j+1]) j=nxt[j];
if(b[i]==a[j+1]) j++;
f[i]=j;
}
}
再次提醒,真正的与保存的值实际应为
for(int i=0;i<strlen(A);++i)
printf("%d ",nxt[i]+1);
for(int i=0;i<strlen(B);++i)
printf("%d ",f[i]+1);
KMPの应用
POJ - 3461 Oulipo
现在再次回到开头的问题 (是不是都快忘掉了)
Q:给定两个字符串A、B,求A在B中出现了多少次
A:求出B的数组后
若有,那么A在B中的一个出现位置就是~
#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long lt;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=2000010;
int Q;
char txt[maxn],pat[maxn];
int nxt[maxn];
void qnxt(char* ss,int len)
{
int j=-1; nxt[0]=-1;
for(int i=1;i<len;++i)
{
while(j!=-1&&ss[i]!=ss[j+1]) j=nxt[j];
if(ss[i]==ss[j+1]) ++j;
nxt[i]=j;
}
}
int KMP(char* a,char* b)
{
int res=0;
int n=strlen(a),m=strlen(b);
int j=-1; qnxt(a,n);
for(int i=0;i<m;++i)
{
while(j!=-1&&b[i]!=a[j+1]) j=nxt[j];
if(b[i]==a[j+1]) ++j;
if(j==n-1) res++;//f[i]=j
}
return res;
}
int main()
{
Q=read();
while(Q--)
{
scanf("%s%s",&pat,&txt);
printf("%d\n",KMP(pat,txt));
}
}
POJ - 2752 Seek the Name, Seek the Fame
Q:给定字符串S,求S中既是前缀也是后缀的子串的所有可能长度
A:求出S的nxt数组
令,不断迭代直到,遍历到的数即为所求
注意这样得到的顺序时降序的,还需要存入栈中再输出
#include<iostream>
#include<cstdio>
#include<cmath>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long lt;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=1000010;
char ss[maxn];
int nxt[maxn],vis[maxn];
int st[maxn],top;
void qnxt(char* a,int len)
{
int j=-1; nxt[0]=-1;
for(int i=1;i<len;++i)
{
while(j!=-1&&a[i]!=a[j+1]) j=nxt[j];
if(a[i]==a[j+1]) j++;
nxt[i]=j;
}
}
int main()
{
while(scanf("%s",&ss)!=EOF)
{
int len=strlen(ss); top=0;
qnxt(ss,len);
int j=len-1;
while(j!=-1){
st[++top]=j+1;
j=nxt[j];
}
while(top) printf("%d ",st[top--]);
printf("\n");
}
return 0;
}