阅读本文的一些注意事项
本文所述的只是 MP M P 算法,KMP算法还需要对 fail f a i l 数组进行优化。但是 MP M P 算法但在竞赛中也能有很好的效果。本文偏向理论证明,如果看完后还是不懂,最好自己手动走几遍算法。我相信实践后,你一定会有更大的收获。
一道例题
naive解法:
暴力枚举 T T 和的匹配起始位置,暴力向后匹配,直到 P P 走完或与不匹配。
暴力优化解法:
暴力枚举 T T 和的匹配起始位置,从 P P 的头和尾同时开始和进行匹配,直到 P P 走完或与不匹配。
这种做法在随机数据中的时间复杂度比上一种优秀得多,但也很容易被出题人卡掉。
正解:
KMP(一种优秀的单个字符串模板匹配算法),由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,所以取名KMP。KMP避免了 P P 在中的重复比较。时间复杂度为理论自由复杂度 O(strlen(P)+strlen(T)) O ( s t r l e n ( P ) + s t r l e n ( T ) )
KMP的实现
如何才能避免重复比较呢?KMP的核心 fail f a i l 数组就能做到这个效果。
如何构建 fail f a i l 数组
KMP的关键在于构造 fail f a i l 数组, faili f a i l i 表示在 P0⋯Pj=Pi−j⋯Pi P 0 ⋯ P j = P i − j ⋯ P i 中 j j 的最大值。对于不存在 P0⋯Pj=Pi−j⋯Pi P 0 ⋯ P j = P i − j ⋯ P i 的情况, faili=0 f a i l i = 0
强行放一段代码(接下来会有解释):
void GetFail() {
int j;
fail[0] = 0, fail[1] = 0;
for (int i = 1; i < n; ++i) {
int j = fail[i];
while (j & P[i] != P[j] ) j = fail[j];
fail[i + 1] = P[i] == P[j] ? j + 1 : 0;
}
}
如何运用 fail f a i l 数组
网上有很多讲解都是顺着继续往下讲的,我看了很多都没有搞懂。
所以我们先假装已经学会得到 fail f a i l (大雾)。那么如何运用 fail f a i l 数组找到 P P 在呢?
首先我们每次判断 Pj P j 是否等于 Ti T i ,如果不相等,则让 j=failj j = f a i l j ,直到两串相等或 j=0 j = 0 时停止循环。退出循环后,如果当前 Pj=Ti P j = T i ,那么 j=j+1 j = j + 1 。如果匹配成功的长度与模式串长度相等也就是 j=strlen(m) j = s t r l e n ( m ) 时, Ans=Ans+1 A n s = A n s + 1
实现代码:
void calc() {
int j = 0;
for (int i = 0; i < n; ++i) {
while (j && T[i] != P[j]) j = fail[j];
if (T[i] == P[j]) ++j;
if (j == m) ++Ans;
}
}
如何保证正确性
很多人看完后可能都会很雾,为什么可以在没匹配成功的情况下 j=failj j = f a i l j 。
下面是一段不那么严谨的证明:
由 fail f a i l 数组定义得 P0⋯Pfailj=Pj−failj⋯Pj P 0 ⋯ P f a i l j = P j − f a i l j ⋯ P j
由 j j 和的定义得 P0⋯Pj=Ti−j⋯Ti P 0 ⋯ P j = T i − j ⋯ T i
通过脑补一波结论我们可以想到 Pj−failj⋯Pj=Ti−failj⋯Ti P j − f a i l j ⋯ P j = T i − f a i l j ⋯ T i
通过等量代换得 P0⋯Pfailj=Ti−failj⋯Ti P 0 ⋯ P f a i l j = T i − f a i l j ⋯ T i
因此,此时将 j=failj j = f a i l j 满足 fail f a i l 的定义,正确性可以保证。
关于时间复杂度
易证可以证明时间复杂度为
O(strlen(P)+strlen(T))
O
(
s
t
r
l
e
n
(
P
)
+
s
t
r
l
e
n
(
T
)
)
简要证明过程如下:
i
i
是外循环,易知最多向后移动
strlen(T)
s
t
r
l
e
n
(
T
)
次。
实践告诉我们
j
j
最多向后移动次
从而得知均摊时间复杂度为
O(strlen(P)+strlen(T))
O
(
s
t
r
l
e
n
(
P
)
+
s
t
r
l
e
n
(
T
)
)
最后再谈谈 fail f a i l 数组为什么这么构造
现在我们已经学会了应用 fail f a i l 数组的方法。再回头看看 fail f a i l 的构造方式。我们会惊奇地发现原来构造 fail f a i l 就是 P P 匹配自己的过程!
KMP完整代码
完整代码:
#include <iostream>
#include <cstdio>
#include <cstring>
const int MAXN = 1e6;
using namespace std;
char T[MAXN + 5], P[MAXN + 5];
int Ans, fail[MAXN + 5], n, m;
void GetFail() {
int j;
fail[0] = 0, fail[1] = 0;
for (int i = 1; i < n; ++i) {
int j = fail[i];
while (j & P[i] != P[j] ) j = fail[j];
fail[i + 1] = P[i] == P[j] ? j + 1 : 0;
}
}
void calc() {
int j = 0;
for (int i = 0; i < n; ++i) {
while (j && T[i] != P[j]) j = fail[j];
if (T[i] == P[j]) ++j;
if (j == m) ++Ans;
}
}
int main() {
scanf("%s%s", T, P);
n = strlen(T), m = strlen(P);
GetFail(), calc();
printf("%d", Ans);
return 0;
}
一些题目
一眼秒的KMP板子题,这题唯一的难点在于要实现的是不可重叠的KMP。其实实现方法一样,只是在每次匹配成功后,强行把变为 0 0 <script type="math/tex" id="MathJax-Element-85">0</script>。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
const int MAXN = 1e6;
using namespace std;
char T[MAXN + 5], P[MAXN + 5];
int fail[MAXN + 5], n, m, Ans;
void GetFail() {
int j;
fail[0] = 0, fail[1] = 0;
for (int i = 1; i <= m; ++i) {
j = fail[i];
while (j && P[i] != P[j]) j = fail[j];
fail[i + 1] = P[i] == P[j] ? j + 1 : 0;
}
}
void calc() {
int j = 0;
for (int i = 0;i <= n; ++i) {
while (j && T[i] != P[j]) j = fail[j];
if (T[i] == P[j]) ++j;
if (j == m) ++Ans, j = 0;
}
}
int main() {
scanf("%s%s", T, P);
n = strlen(T), m = strlen(P);
GetFail(), calc();
printf("%d\n", Ans);
return 0;
}