KMP笔记(1)

文章介绍了KMP算法,一种用于字符串匹配的高效算法。首先从暴力解法出发,指出其存在的冗余,然后引入KMP算法的优化思路。通过分析模式串的前后缀关系,构建next数组来存储最长公共前后缀的长度,避免重复匹配。最后,给出了优化后的AC代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

KMP算法又称Knuth-Morris-Pratt算法,我们一般结合题型,从暴力算法开始理解,再对暴力算法进行优化。

假设我们面对这样一个问题:

 我们先想想暴力怎么做这道题:

int s[N], p[M];
//中间输入暂且省略
for  (int i = 0; i < n; i ++) {
    bool flag = true;
    for (int j = 0; j < m; j ++) {
        if (s[i + j - 1] != q[j]) {
            flag = false;
            break;
        }
    }
    if (flag) cout << i - m + 1 << " "; 
}

但是我们很快可以发现,我们在多次遍历的时候有很多信息被重复使用,比如我们在内循环从第一位遍历到第五位,在第六位不匹配了,所以我们break然后我们开始下一个外循环,但是我们在之前的遍历中已经遍历过了第1-5位,现在往后挪了一位还要重新再遍历一遍,这里我们其实可以获取一些信息,这也正是该优化的地方。

我们首先考虑这种情况:长度为m的模式串p,在s的某个位置匹配成功了,我们接下来将p往后推移再次尝试匹配,假设偏移了x位又成功了,而x<m,也即此时第二次匹配的模式串p和第一次匹配成功的位置有所重合,请思考一下,这是否意味着,在p模式串本身之中,前几个字母,是否重复出现了呢?如下

sabaabaabc
p1abaab
p2abaab

p两次匹配成功的位置产生了重合,这就意味着p的前几位和后几位产生了重复,如上的“ab”。我们把这两个相等的部分分别成为前缀和后缀。

面对 KMP 这种复杂的算法,我们必须要将其拆解,逐个击破。假设有一个字符串 p (下标从 0 开始),那么它以 i 号位作为结尾的子串就是 p[0…i]。对该子串来说,长度为 k + 1 的前缀与后缀分别是 p[0…k] 与 p[i-k…i]。我们构造一个 int 型数组( 一般记作next)。其中,next[i] 表示使字串 p[0…i] 中前缀 p[0…k] 等于后缀 p[i-k…i] 的最大的 k。(其中,这个相等的前缀和后缀不能是 p[0…i] 本身。);如果找不到相等的前后缀,就令 next[i] = -1。显然,next[i] 就是所求最长相等前后缀中前缀的最后一位的下标。

还拿上面的例子来说,对于p:

如果i = 0,那么 p[0…i] = a,由于前后缀不能是p本身,所以此时重复前后缀的长度为0 = k + 1,也即next[i] = next[0] = k = -1;

如果i = 1,那么 p[0…i] = ab,和上面同理,next[1] = -1;

如果i = 2,那么 p[0…i] = aba,我们找到相同的前后缀“a”,长度为1= k + 1,所以next[i] = next[2] = k = 0;

如果i = 3,那么 p[0…i] = abaa,我们找到相同的前后缀“ab”,长度为1 = k + 1,所以next[3] = 0;

如果i = 4,那么 p[0…i] = abaab,分别存在相同前后缀“a”以及“ab”,但是由于我们要最大程度上进行优化,所以一直取最大的k值,所以我们这里取“ab”,长度为2 = k + 1,所以next[4] = k = 1;

但是我们该怎么最优求出next呢?暴力当然不是最优,我们应该选择递归的做法,用next[0]……next[i - 1]来求出next[i],

在此过程中,我们需要进行判定,如果p[i] == p[next[i - 1] + 1],即新加入的一个p[i]使得我们的next增加,那么我们就直接让next[i] = next[i - 1]  + 1即可。

如果不是,我们还需要递归,由于我们后面还需要对next[i - 1]进行运算,所以我们需要创建一个j 来表示上一次成功匹配时的next值,然后不断让j = next[j],直到 j 回到 -1或是中途p[i] == p[j + 1]即可。

优化后AC代码如下,建议慢慢理解:

#include <iostream>
using namespace std;

const int N = 100010, M = 1000010;
int n, m;
int next[N];
char s[M], p[N];

int main () {
    cin >> n >> p + 1 >> m >> s + 1;

    for (int i = 2, j = 0; i <= n; i ++) {
        while (j && p[i] != p[j + 1]) j = next[j];
        if (p[i] == p[j + 1]) j ++ ;
        next[i] = j;
    }

    for (int i = 1, j = 0; i <= m; i ++) {
        while (j && s[i] != p[j + 1]) j = next[j];
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n) {
            printf("%d ", i - n);
            j = next[j];
        }
    }
    return 0;
}

参考如下:

AcWing 831. KMP字符串 —— 深入浅出 next 数组 - AcWing

字符串匹配的KMP算法--前缀和后缀的详解_kmp算法前后缀_与时俱进2014的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值