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;
}