概念
( ext{KMP}) 算法,又称模式匹配算法,能够在 (O(n+m)) 的时间复杂度内求解如下的问题:
有两个字符串 (p[n]) 、(s[m]) ,称字符串 (p) 为模式串、字符串 (s) 为文本串。要求判断模式串 (p) 是否为文本串 (s) 的子串,并求出模式串 (p) 在文本串 (s) 中出现的位置。
算法流程
- 对模式串 (p) 进行自我匹配
对于模式串 (p) 第 (i) 位,求出最大的位数 (f[i]) ,使得模式串 (p) 中以第 (i) 位为结尾且长度为 (f[i]) 的子串和模式串 (p) 前 (f[i]) 位组成的字符串重合。这里的 (f) 数组就是模式串 (p) 的失配数组。
- 扫描一遍文本串 (s) ,利用 (f) 数组寻找模式串 (p)
假设现在文本串 (s) 已经匹配到第 (i) 位,模式串 (p) 已经匹配到第 (j) 位,此时会出现以下两种情况:
若 (s[i]=p[j]) ,即当前字符匹配成功,就令 (i++) , (j++) ,继续匹配下一对字符;
若 (s[i]≠p[j]) ,即当前字符匹配失败(也称“失配”),由于模式串 (p) 前 (f[j]) 位组成的字符串与文本串 (s) 第 (i) 位前 (f[j]) 位组成的字符串依然重合(失配数组 (f) 的作用),我们就令 (j=f[j]) ,也就相当于把模式串 (p) 向右移动了 (j-f[j]) 位,重新判断当前的 (s[i]) 是否等于 (p[j]) 。
当然,如果已经达到 (j=n) 的时候,就说明当前模式串 (p) 已经在文本串 (s) 中完全匹配,我们便可以记录下答案。
如此下去,便能够求出模式串 (p) 在文本串 (s) 中所有的位置,由此达到我们的目的。
代码
- 求模式串 (p) 的失配数组 (f) :
for(int i=2,j=0;i<=n;i++)
{
while(j>0&&p[i]!=p[j+1])
{
j=f[j];
}
if(p[i]==p[j+1])
{
j++;
}
f[i]=j;
}
- 文本串 (s) 与模式串 (p) 的匹配过程:
for(int i=1,j=0;i<=m;i++)
{
while(j>0&&s[i]!=p[j+1])
{
j=f[j];
}
if(s[i]==p[j+1])
{
j++;
}
if(j==n)
{
printf("%d
",i-n+1);
j=f[j];
}
}
典型例题
( ext{Solution}) :
这是一道求失配数组 (f) 的题,比较基础。
#include<iostream>
#include<cstdio>
#define Re register
using namespace std;
const int SIZE=2000001;
char st[SIZE];
int L,f[SIZE];
int main()
{
scanf("%d",&L);
for(Re int i=1;i<=L;i++)
{
cin>>st[i];
}
for(Re int i=2,j=0;i<=L;i++)
{
while(j>0&&st[i]!=st[j+1])
{
j=f[j];
}
if(st[i]==st[j+1])
{
j++;
}
f[i]=j;
}
printf("%d",L-f[L]);
return 0;
}
( ext{Solution}) :
此题用到失配数组 (f) 的性质:对于每个 (i) 而言,长度为 (f[i]) 的前缀与长度为 (f[i]) 的后缀是相等的。
由题意可知,如果 (i) 有一个公共的前后缀,其长度为 (l) ,那么这个前缀 (i) 就有一个周期为 (i-l) 。
于是我们用上失配数组 (f) 求解即可。
但是,本题求的是最大的周期,因此我们应该求出最小的 (l) 满足题意。
当然,这里可以用“记忆化”的思想来简化时间复杂度。
#include<iostream>
#include<cstdio>
#define Re register
#define LL long long
using namespace std;
const int SIZE=2000001;
char st[SIZE];
int L,f[SIZE];
LL Ans;
int main()
{
scanf("%d",&L);
scanf("%s",st+1);
for(Re int i=2,j=0;i<=L;i++)
{
while(j>0&&st[i]!=st[j+1]) j=f[j];
if(st[i]==st[j+1]) j++;
f[i]=j;
}
for(Re int i=1;i<=L;i++)
{
int Now=i;
while(f[Now]>0) Now=f[Now];
if(f[i]>0) f[i]=Now;
Ans+=(i-Now);
}
printf("%lld",Ans);
return 0;
}
( ext{Solution}) :
此题在 ( ext{KMP}) 算法的基础上加了差分的思想。
对于每一个 (1≤i≤card(O)) ,在每次用 ( ext{KMP}) 算法找到 (s) 中 (O[i]) 所在的位置 ([L,R]) 时,就修改一下差分数组。
最后对差分数组求前缀和,大于 (0) 即为满足题意的长度,输出最大的长度即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#define Re register
using namespace std;
char opt[301][21],st[300001];
int n,m;
int Len[301];
int f[301];
int dif[300001];
int main()
{
while(1)
{
scanf("%s",opt[++m]+1);
if(opt[m][1]=='.') break;
Len[m]=strlen(opt[m]+1);
}
m--;
while(scanf("%s",st+n+1)!=EOF)
{
n=strlen(st+1);
}
for(Re int k=1;k<=m;k++)
{
memset(f,0,sizeof f);
for(Re int i=2,j=0;i<=Len[k];i++)
{
while(j>0&&opt[k][i]!=opt[k][j+1])
{
j=f[j];
}
if(opt[k][i]==opt[k][j+1])
{
j++;
}
f[i]=j;
}
for(Re int i=1,j=0;i<=n;i++)
{
while(j>0&&st[i]!=opt[k][j+1])
{
j=f[j];
}
if(st[i]==opt[k][j+1])
{
j++;
}
if(j==Len[k])
{
dif[i-Len[k]+1]++;
dif[i+1]--;
j=f[j];
}
}
}
for(Re int i=1;i<=n;i++)
{
dif[i]+=dif[i-1];
if(dif[i]<=0)
{
printf("%d",i-1);
return 0;
}
}
printf("%d",n);
return 0;
}
( ext{Solution}) :
此题还用到了栈 ( ext{stack}) 的思想。
在 ( ext{KMP}) 算法过程中,把遍历 (S) 时遍历到的 (i) 入栈(本题栈中存的最好是下标)。
当匹配上 (T) 时,就把匹配成功的部分出栈,然后 (j) 从栈中所能匹配到的最大的位置开始,即 (j=f[stack.len]) ,继续做下去。
最后把栈中的元素输出即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#define Re register
using namespace std;
const int SIZE=2000001;
char s[SIZE],t[SIZE];
int n,m,Len;
int f[SIZE];
int Last[SIZE];
int Stack[SIZE];
int main()
{
scanf("%s",s+1);
scanf("%s",t+1);
n=strlen(s+1);
m=strlen(t+1);
for(Re int i=2,j=0;i<=m;i++)
{
while(j>0&&t[i]!=t[j+1])
{
j=f[j];
}
if(t[i]==t[j+1])
{
j++;
}
f[i]=j;
}
for(Re int i=1,j=0;i<=n;i++)
{
while(j>0&&s[i]!=t[j+1])
{
j=f[j];
}
if(s[i]==t[j+1])
{
j++;
}
Last[i]=j;
Stack[++Len]=i;
if(j==m)
{
Len-=m;
j=Last[Stack[Len]];
}
}
for(Re int i=1;i<=Len;i++)
{
printf("%c",s[Stack[i]]);
}
return 0;
}
总结
( ext{KMP}) 算法的思想在于把模式串 (p) 的失配数组 (f) 用 (O(N)) 的时间复杂度预处理,并在与文本串 (s) 匹配的过程中起到关键性的作用:匹配失败就跳到下一个有可能的位置再匹配。相比较于朴素算法,优化了很多不必要的匹配过程。而这也是 ( ext{KMP}) 算法中最为重要的思想所在。