KMP模式匹配
概念
\(\text{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\n",i-n+1);
j=f[j];
}
}
典型例题
\(\text{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;
}
\(\text{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;
}
\(\text{Solution}\) :
此题在 \(\text{KMP}\) 算法的基础上加了差分的思想。
对于每一个 \(1≤i≤card(O)\) ,在每次用 \(\text{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;
}
\(\text{Solution}\) :
此题还用到了栈 \(\text{stack}\) 的思想。
在 \(\text{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;
}
总结
\(\text{KMP}\) 算法的思想在于把模式串 \(p\) 的失配数组 \(f\) 用 \(O(N)\) 的时间复杂度预处理,并在与文本串 \(s\) 匹配的过程中起到关键性的作用:匹配失败就跳到下一个有可能的位置再匹配。相比较于朴素算法,优化了很多不必要的匹配过程。而这也是 \(\text{KMP}\) 算法中最为重要的思想所在。