算法刷题【洛谷P3375】【模板】KMP字符串匹配(KMP算法模板 超详细易懂讲解)

异想之旅:本人原创博客完全手敲,绝对非搬运,全网不可能有重复;本人无团队,仅为技术爱好者进行分享,所有内容不牵扯广告。本人所有文章仅在CSDN、掘金和个人博客(一定是异想之旅域名)发布,除此之外全部是盗文!


洛谷 P3375 【模板】KMP字符串匹配

题目描述

给出两个字符串 s 1 s_1 s1 s 2 s_2 s2,若 s 1 s_1 s1 的区间 [ l , r ] [l, r] [l,r] 子串与 s 2 s_2 s2 完全相同,则称 s 2 s_2 s2 s 1 s_1 s1 中出现了,其出现位置为 l l l
现在请你求出 s 2 s_2 s2 s 1 s_1 s1 中所有出现的位置。

定义一个字符串 s s s 的 border 为 s s s 的一个 s s s 本身的子串 t t t,满足 t t t 既是 s s s 的前缀,又是 s s s 的后缀。
对于 s 2 s_2 s2,你还需要求出对于其每个前缀 s ′ s' s 的最长 border t ′ t' t 的长度。

输入格式

第一行为一个字符串,即为 s 1 s_1 s1

第二行为一个字符串,即为 s 2 s_2 s2

输出格式

首先输出若干行,每行一个整数,按从小到大的顺序输出 s 2 s_2 s2 s 1 s_1 s1 中出现的位置。

最后一行输出 ∣ s 2 ∣ |s_2| s2 个整数,第 i i i 个整数表示 s 2 s_2 s2 的长度为 i i i 的前缀的最长 border 长度。

输入输出样例

In 1:

ABABABC
ABA

Out 1:

1
3
0 0 1

样例解释:

对于 s 2 s_2 s2 长度为 3 3 3 的前缀 ABA,字符串 A 既是其后缀也是其前缀,且是最长的,因此最长 border 长度为 1 1 1

数据范围

对于全部的测试点,保证 1 ≤ ∣ s 1 ∣ , ∣ s 2 ∣ ≤ 1 0 6 1 \leq |s_1|,|s_2| \leq 10^6 1s1,s2106 s 1 , s 2 s_1, s_2 s1,s2 中均只含大写英文字母。

题解

如题所述,这就是一道纯粹的模板题。不要被输出说明的第二句吓到,他其实就是让你输出会跳指针罢了。

下面我们来讲解KMP算法。

首先假设我们要在字符串 ①ABAABABACB 中匹配 ②ABABA

最简单的暴力方法,分别从①的第一位,第二位,第三位……开始截取长度和②相等的字符串,逐字符判断该产生的字符串和②是否一样。

画图表示如下:

在这里插入图片描述

想必这个方法大家都会吧,复杂度近似 O ( n m ) O(nm) O(nm) 也易得。

现在让我们来注意这两次尝试

在这里插入图片描述

(为方便描述,第一行加粗的为原串,往下的六行分别为模式串的第 i i i 次匹配)

如果人脑模拟到第 1 1 1 次匹配的位置,我们会发现前四个能够正常匹配的字符中,有两个重复的 AB ,那么顺理成章把前两个 AB 放到后两个 AB 的位置即可(也就是直接跳到第 3 3 3 次匹配的状态)。每次都这么做,效率显然可以大大提高。

也就是说,我们每次出现匹配失败的位置后,模式串不非得直接去匹配原串的下一个字符,而可以直接多跳一些。这就是KMP算法的核心。

那么我们怎么找每次跳多少个呢?

如果我们在模式串的 q q q 位置和原串的 p p p 位置尝试匹配时失配了,那么就看看模式串从第 1 1 1 位到第 q − 1 q - 1 q1 位的字符构成的字符串 前缀后缀 最大的相同长度。注意,这里的前缀和后缀长度都不能等于原串。我们称这个“最大的相同长度”为最大公共元素长度。显然,当模式串的 q q q 位置失配,我把模式串能与它本身 [ q − 1 − z , q − 1 ] [q-1-z, q-1] [q1z,q1] 这个区间内的字符匹配的 [ 1 , z ] [1,z] [1,z] 部分放过来到 p p p 以前,那么就可以在不让 p p p 回溯的情况下继续去求解了。

我承认自己语文不好,放张图理解。由于这里有一张很清晰的图片,我就不再重复工作了。图片中是对一个值为 ABCDABD 的模式串做分析。

在这里插入图片描述

图片来源:从头到尾彻底理解KMP(2014年8月22日版)_v_JULY_v的博客-CSDN博客_串的next数组怎么求。说句题外话,这个博主写的是我目前发现的全网描述最细致的了,我也是最开始看这个理解的,看我的语言风格搞不懂的同学们可以去换个口味。

那么对于本文的模式串 ABABA ,求解的结果应该是这样子的

在这里插入图片描述

让我们回到这张图:

在这里插入图片描述

显然当第 1 1 1 次匹配失配时,我们已经尝试了 4 4 4 个字符,对应的模式串最大公共元素长度为 2 2 2 ,所以我们可以直接把模式串向后移动 2 2 2 位,而不是仅仅 1 1 1 位。

得出结论:失配时,模式串向右移动的位数为 已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

回到原题,我们的AC代码如下:


#include <cstring>
#include <iostream>
const int MAXN = 1000010;
using namespace std;
int kmp[MAXN];
char a[MAXN], b[MAXN];
int main() {
    cin >> a + 1 >> b + 1;  // 从第一位开始输入,个人习惯而已不必纠结
    int la = strlen(a + 1), lb = strlen(b + 1);  // 求长度
    for (int i = 2, j = 0; i <= lb; i++) {
        // 构建kmp数组,即计算最大公共元素长度
        while (j > 0 && b[i] != b[j + 1]) j = kmp[j];
        if (b[j + 1] == b[i]) j++;
        kmp[i] = j;
    }
    for (int i = 1, j = 0; i <= la; i++) {
        // 真正的匹配过程
        while (j > 0 && b[j + 1] != a[i]) j = kmp[j];
        if (b[j + 1] == a[i]) j++;
        if (j == lb) {
            cout << i - lb + 1 << endl;
            j = kmp[j];
        }
    }

    for (int i = 1; i <= lb; i++) cout << kmp[i] << " ";  // 打印kmp数组
}

毕竟在代码里,我们是无法移动字符串的,那我们能做什么呢?

——构建指针,移动指针

先看匹配过程。 i i i 是表示目前正在匹配原串的第 i i i 位, j j j 表示正在用模式串的第 j j j 位与之匹配。b[j + 1] != a[i] 表示当前状态已经失配,需要将模式串右移,也就是将模式串指针左移。模式串右移的长度是 已匹配字符数 - 失配字符的上一位字符所对应的最大长度值 ,那么指针左移的长度就是 已匹配字符数 - (已匹配字符数 - 失配字符的上一位字符所对应的最大长度值) ,也就是 失配字符的上一位字符所对应的最大长度值

那么为啥 j > 0 j>0 j>0 呢?你可以暂且理解为特判——如果 j j j 已经等于 0 0 0 了还不跳出不就死循环了嘛……

b[j + 1] == a[i]j++ 好理解是要继续匹配模式串下一位,而这个条件判断则有两种情况。当 j j j 仍大于 0 0 0 时,那么该条件一定满足(否则无法跳出while);当 j j j 已经减小到 0 0 0 时,那么就真的需要好好看看能不能开始下一次匹配了。不行?那就让 i++ 之后再开始匹配。

j == lb ,代表模式串被匹配完了,那么我们就输出这个匹配位置的起始坐标,不用再说了。

最后,kmp数组,也就是最大公共元素长度,我们是如何求得的?

看代码

一定要仔细看

有没有发现

其实和匹配过程一模一样,只不过这里是用部分模式串去匹配整个模式串。

我们很容易发现在上面的匹配过程中,任何一次for循环结束时,原串的第 i i i 个字符和模式串的第 j j j 个字符都是一样的,除非 j j j 等于 0 0 0 。而构建kmp数组的过程中指针 i i i 2 2 2 开始,可以保证匹配到的不会是从模式串头部开始的一模一样的区间。那么:

  • j j j 不等于 0 0 0 ,此时模式串前 j j j 位与后 i − j i-j ij 位匹配,求得此时对应的最大公共元素长度为 j j j
  • j j j 等于 0 0 0 ……嗯……那就是没匹配上,最大公共元素长度为 0 0 0 呗,合情合理

结束。

思考一个问题:上文已经很清晰说明了KMP算法求得的解一定正确,但是有没有漏解的情况?

要凌晨三点了,太困了,关于正确性的证明先鸽了,大家可以看看这篇文章,写的真的很好,我要补充大概也是从他这里拿思路哈哈哈哈:反证法证明:为什么KMP算法不会跳过(漏掉)正确的答案_马小超i的博客-CSDN博客_kmp算法为什么不会漏解

在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

异想之旅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值