KMP笔记(1)

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的博客-CSDN博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法C++代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值