没事找事写算法(3)---KMP

阅读本文的一些注意事项

本文所述的只是 MP M P 算法,KMP算法还需要对 fail f a i l 数组进行优化。但是 MP M P 算法但在竞赛中也能有很好的效果。本文偏向理论证明,如果看完后还是不懂,最好自己手动走几遍算法。我相信实践后,你一定会有更大的收获。

一道例题

HazeOJ #72
这里写图片描述

naive解法:

暴力枚举 T T P的匹配起始位置,暴力向后匹配,直到 P P 走完或与T不匹配。

暴力优化解法:

暴力枚举 T T P的匹配起始位置,从 P P 的头和尾同时开始和T进行匹配,直到 P P 走完或与T不匹配。

这种做法在随机数据中的时间复杂度比上一种优秀得多,但也很容易被出题人卡掉。

正解:

KMP(一种优秀的单个字符串模板匹配算法),由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,所以取名KMP。KMP避免了 P P T中的重复比较。时间复杂度为理论自由复杂度 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 表示在 P0Pj=PijPi P 0 ⋯ P j = P i − j ⋯ P i j j 的最大值+1。对于不存在 P0Pj=PijPi 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 T呢?

首先我们每次判断 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 数组定义得 P0Pfailj=PjfailjPj P 0 ⋯ P f a i l j = P j − f a i l j ⋯ P j

j j i的定义得 P0Pj=TijTi P 0 ⋯ P j = T i − j ⋯ T i

通过脑补一波结论我们可以想到 PjfailjPj=TifailjTi P j − f a i l j ⋯ P j = T i − f a i l j ⋯ T i

通过等量代换得 P0Pfailj=TifailjTi 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 是外循环,易知i最多向后移动 strlen(T) s t r l e n ( T ) 次。
实践告诉我们 j j 最多向后移动strlen(P)
从而得知均摊时间复杂度为 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;
}

一些题目

HazeOJ #78
这里写图片描述

一眼秒的KMP板子题,这题唯一的难点在于要实现的是不可重叠的KMP。其实实现方法一样,只是在每次匹配成功后,强行把j变为 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值