Q:给定两个字符串A、B,求A在B中出现了多少次
这是KMP的经典问题,我们将从这里开始引入KMP算法
KMP算法の思想
KMP算法分为两步
1.对A进行自我匹配,求出A的nxt数组
其中
n
x
t
[
i
]
nxt[i]
nxt[i]表示 A中以
i
i
i结尾的非前缀子串 与 A的前缀的最大匹配长度
即若
n
x
t
[
i
]
=
j
nxt[i]=j
nxt[i]=j,则
A
[
i
−
j
+
1
A[i-j+1
A[i−j+1~
i
]
=
A
[
1
i]=A[1
i]=A[1 ~
j
]
j]
j]
2.将A与B匹配,求出数组
f
f
f
其中
f
[
i
]
f[i]
f[i]表示 B中以
i
i
i结尾的子串 与 A的前缀的最大匹配长度
煮个栗子
那么该如何快速求出nxt数组呢
假设我们已经知道
n
x
t
[
1
]
nxt[1]
nxt[1]~
n
x
t
[
6
]
nxt[6]
nxt[6],现在求
n
x
t
[
7
]
nxt[7]
nxt[7]
求
n
x
t
[
7
]
nxt[7]
nxt[7]得过程实际上就是 求一个最大的
j
j
j,满足
A
[
1
A[1
A[1~
j
]
=
A
[
i
−
j
j] = A[i-j
j]=A[i−j~
6
]
6]
6]且
A
[
j
+
1
]
=
A
[
7
]
A[j+1]=A[7]
A[j+1]=A[7]
然后令
n
x
t
[
7
]
=
j
+
1
nxt[7]=j+1
nxt[7]=j+1
对于这个找
j
j
j得过程,我们可以按照从大到小的顺序
先找到所有满足第一个条件的
j
j
j,再判断第二个条件是否成立
对于
n
x
t
[
7
]
nxt[7]
nxt[7],满足第一个条件的最大的
j
j
j显然是
j
=
n
x
t
[
6
]
=
4
j=nxt[6]=4
j=nxt[6]=4
然后我们判断第二个条件
A
[
j
+
1
=
5
]
=
A
[
i
=
7
]
A[j+1=5]=A[i=7]
A[j+1=5]=A[i=7]成立,所以
n
x
t
[
7
]
=
5
nxt[7]=5
nxt[7]=5
在考虑
n
x
t
[
8
]
nxt[8]
nxt[8],同样先取最大的满足条件一的
j
=
n
x
t
[
7
]
=
5
j=nxt[7]=5
j=nxt[7]=5
但这时候发现
A
[
j
+
1
=
6
]
!
=
A
[
i
=
8
]
A[j+1=6]!=A[i=8]
A[j+1=6]!=A[i=8],条件二不成立
于是我们再找第二大的满足条件一
j
j
j,而这个
j
j
j正好就是
n
x
t
[
n
x
t
[
7
]
]
=
n
x
t
[
5
]
=
3
nxt[nxt[7]]=nxt[5]=3
nxt[nxt[7]]=nxt[5]=3
但是
A
[
j
+
1
=
4
]
!
=
A
[
i
=
8
]
A[j+1=4]!=A[i=8]
A[j+1=4]!=A[i=8],条件二依然不成立
再找第三大的满足条件一
j
j
j,这个
j
j
j等于
n
x
t
[
n
x
t
[
n
x
t
[
7
]
]
]
=
n
x
t
[
3
]
=
1
nxt[nxt[nxt[7]]]=nxt[3]=1
nxt[nxt[nxt[7]]]=nxt[3]=1
很可惜条件二还是不成立
再找第四大的满足条件一
j
=
n
x
t
[
1
]
=
0
j=nxt[1]=0
j=nxt[1]=0,到这里前面已经没有字符了
所以直接判断是否有
A
[
1
]
=
A
[
i
=
8
]
A[1]=A[i=8]
A[1]=A[i=8],这里满足了条件,所以
n
x
t
[
8
]
=
1
nxt[8]=1
nxt[8]=1
通过上面的 栗子 我们已经对KMP的思路有了基本了解
在这里再一次用稍形式化的语言描述一次
假设当前已求出
n
x
t
[
1
]
nxt[1]
nxt[1]~
n
x
t
[
i
−
1
]
nxt[i-1]
nxt[i−1]
现在需要求一个最大的
j
j
j,满足
A
[
1
A[1
A[1~
j
]
=
A
[
i
−
j
j] = A[i-j
j]=A[i−j~
i
−
1
]
i-1]
i−1]且
A
[
j
+
1
]
=
A
[
i
]
A[j+1]=A[i]
A[j+1]=A[i]
令
n
x
t
[
i
]
=
j
+
1
nxt[i]=j+1
nxt[i]=j+1
首先尝试最大的满足条件一的
j
=
n
x
t
[
i
−
1
]
j=nxt[i-1]
j=nxt[i−1],判断条件二是否成立
若成立则
n
x
t
[
i
]
=
j
+
1
nxt[i]=j+1
nxt[i]=j+1,否则
j
=
n
x
t
[
j
]
j=nxt[j]
j=nxt[j],重复上述步骤
当
j
=
0
j=0
j=0时直接判断是否有
A
[
1
]
=
A
[
i
]
A[1]=A[i]
A[1]=A[i],若是则
n
x
t
[
i
]
=
1
nxt[i]=1
nxt[i]=1,否则等于0
算法的总时间复杂度为 O ( n ) O(n) O(n)
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;
}
}
求完
n
x
t
[
]
nxt[]
nxt[]数组后在考虑
f
[
]
f[]
f[]数组
由于其定义的基本相同,所以求解方法已基本一致
注意为了运算方便保存在
f
[
]
f[]
f[]中的值也是减了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;
}
}
再次提醒,真正的 n x t [ ] nxt[] nxt[]与 f [ ] f[] f[]保存的值实际应为
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的
f
[
]
f[]
f[]数组后
若有
f
[
i
]
=
n
f[i]=n
f[i]=n,那么A在B中的一个出现位置就是
B
[
i
−
n
+
1
B[i-n+1
B[i−n+1~
i
]
i]
i]
#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数组
令
j
=
l
e
n
j=len
j=len,不断迭代
j
=
n
x
t
[
j
]
j=nxt[j]
j=nxt[j]直到
j
=
0
j=0
j=0,遍历到的数即为所求
注意这样得到的顺序时降序的,还需要存入栈中再输出
#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;
}