前言
哎呀!我们来刷题吧!
来看看这一道题:
有两个字符串 S S S和 T T T
求 T T T出现在 S S S中的第一个位置
1 ≤ ∣ S ∣ ≤ 1 0 6 1\le|S|\le10^6 1≤∣S∣≤106 1 ≤ ∣ T ∣ ≤ 1 0 6 1\le|T|\le10^6 1≤∣T∣≤106
我和我的小伙伴们都惊呆了,这没法用 O ( ∣ S ∣ × ∣ T ∣ ) O(|S|\times|T|) O(∣S∣×∣T∣)做啊,那怎么办呢?
我们就可以用KMP算法了!
KMP
啥是KMP
在计算机科学中,Knuth-Morris-Pratt 字符串查找算法(简称:KMP 算法)可在一个 文本串 T T T 内查找一个 模式串 P P P 的出现位置。该算法通过对这个词在不匹配时本身包含的信息来确定下一个匹配将在哪里开始的发现,从而避免重新检查先前匹配的字符,提高程序运行效率。
具体来说,KMP 算法在匹配前会预处理模式串 P P P,得到一个 f a i l fail fail 数组。借助 failfailfail 数组,可以在匹配过程中减少很多冗余的匹配操作,由此提高了算法的效率。KMP 算法的时间复杂度为 O ( n + m ) \mathcal{O}(n+m) O(n+m),其中 n n n 和 m m m 分别表示两个串的长度。
关于fail数组
KMP 算法的核心是 f a i l fail fail 数组 对于字符串 s = s 0 s 1 … s n − 1 s=s_0 s_1\ldots s_{n-1} s=s0s1…sn−1 ,如果 jjj 是满足 s 0 … j = s i − j … i s_{0\ldots j}=s_{i-j \ldots i} s0…j=si−j…i 的最大值,则 f a i l i = j fail_i=j faili=j(注意 j < i j< i j<i),其中 s a … b s_{a \ldots b} sa…b 表示字符串 s s s 从下标 a a a 到下标 b b b 的子串,即 s a s a + 1 … s b − 1 s b s_as_{a+1}\ldots s_{b-1}s_b sasa+1…sb−1sb 对于不存在 s 0 … j = s i − j … i s_{0\ldots j}=s_{i-j \ldots i} s0…j=si−j…i的情况, f a i l i = − 1 fail_i=-1 faili=−1
例如,对于字符串aababaab
,则
f
a
i
l
fail
fail 值如下表所示:
a | a | b | a | b | a | a | b | |
---|---|---|---|---|---|---|---|---|
f a i l fail fail | − 1 -1 −1 | 0 0 0 | − 1 -1 −1 | 0 0 0 | − 1 -1 −1 | 0 0 0 | 1 1 1 | 2 2 2 |
匹配过程
理解了 f a i l fail fail,我们先不急着研究怎么计算 f a i l fail fail,而是先看看如何借助 f a i l fail fail值快速地进行字符串匹配
我们假设
f
a
i
l
fail
fail数组已经正确计算出来了。
对于上图比较过程,当’a’和’?‘和比较的时候比较失败了,那么我们需要把母串的起始位置移动到下一个位置。对于
2
2
2 指向的匹配,这时候我们没有必要去从头开始一个一个比较,我们已经知道红色这一整块中间必然会有地方失配(失配就是匹配失败),为什么呢? 因为我们知道
f
a
i
l
[
i
]
=
1
fail[i] = 1
fail[i]=1,如果红色的块能完美匹配,那么
f
a
i
l
[
i
]
fail[i]
fail[i] 应该等于
4
4
4,这和已知的
f
a
i
l
[
i
]
=
1
fail[i]=1
fail[i]=1 矛盾。第
3
3
3,
4
4
4 次匹配过程也是同样的道理,没必要比较了,根据已知信息可以推导出来一定不可能匹配上了。所以我们直接跳到
f
a
i
l
[
i
]
+
1
fail[i] + 1
fail[i]+1的位置进行匹配,也就是直接跳到匹配
5
5
5,因为’?'前面的串我们知道一定能和
f
a
i
l
[
i
]
+
1
fail[i] + 1
fail[i]+1前面的串匹配。 这就是 KMP 匹配的本质原理,利用已经计算过的信息加速匹配过程。
板子
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 100;
int fail[maxn];
void getFail(char *P){
int m=strlen(P);
fail[0]=-1;
for(int i=1;i<m;i++)
{
int j=fail[i-1];
while(j>=0 && P[j+1]!=P[i]){
j=fail[j];
}
if(P[j+1]==P[i]){
j++;
}
fail[i]=j;
}
}
int KMP(char *T,char *P){
int n=strlen(T),m=strlen(P);
int j=-1;
for(int i=0;i<n;i++){
while(j>=0 && P[j+1]!=T[i]){
j=fail[j];
}
if(P[j+1]==T[i]){
j++;
if(j+1==m){
return i-m+1;
}
}
}
return -1;
}
int main() {
char s[maxn],t[maxn];
cin>>s>>t;
getFail(t);
cout<<KMP(s,t)<<endl;
return 0;
}
解释代码
int KMP(char *T,char *P){
int n=strlen(T),m=strlen(P);
int j=-1;
for(int i=0;i<n;i++){
while(j>=0 && P[j+1]!=T[i]){
j=fail[j];
}
if(P[j+1]==T[i]){
j++;
if(j+1==m){
return i-m+1;
}
}
}
return -1;
}
KMP板子
void getFail(char *P){
int m=strlen(P);
fail[0]=-1;
for(int i=1;i<m;i++)
{
int j=fail[i-1];
while(j>=0 && P[j+1]!=P[i]){
j=fail[j];
}
if(P[j+1]==P[i]){
j++;
}
fail[i]=j;
}
}
求 f a i l fail fail数组
结语
好了,今天的KMP算法已经讲完了,如果有问题大家可以提出来!