KMP算法

KMP算法介绍

KMP(Knuth-Morris-Pratt)算法是一种高效的字符串匹配算法,用于在主串(text)中查找模式串(pattern)。KMP通过预处理模式串,避免了在匹配失败时回溯主串,提高了匹配效率。其时间复杂度为 O(n + m),其中 n 是主串的长度,m 是模式串的长度。

KMP算法的关键思想

KMP算法的核心思想是使用部分匹配表(也叫做“前缀函数”或“失配函数”,即 next 数组)来记录模式串中部分匹配的长度。当出现不匹配时,模式串通过 next 数组决定从哪里重新开始匹配,而不是从头开始。

next 数组的作用

next[i] 表示当模式串中的第 i 个字符失配时,应该跳回到模式串的第 next[i] 个字符继续匹配。这个值代表了模式串中已匹配部分的最长相同前缀和后缀的长度。

KMP算法的步骤

  1. 计算 next 数组

    • next[i] 表示模式串 p[0:i] 的前缀和后缀中最长的相同部分的长度。
    • 如果字符匹配,next[i] 增加;如果不匹配,参考之前的部分匹配跳转。
  2. 主串与模式串匹配

    • 在匹配主串时,如果字符不匹配,利用 next 数组来确定模式串应该跳转的位置,避免重新开始匹配。

题目

给定一个字符串 SS,以及一个模式串 PP,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 PP 在字符串 SS 中多次作为子串出现。

求出模式串 PP 在字符串 SS 中所有出现的位置的起始下标。

输入格式

第一行输入整数 NN,表示字符串 PP 的长度。

第二行输入字符串 PP。

第三行输入整数 MM,表示字符串 SS 的长度。

第四行输入字符串 SS。

输出格式

共一行,输出所有出现位置的起始下标(下标从 00 开始计数),整数之间用空格隔开。

数据范围

1≤N≤1051≤N≤105
1≤M≤1061≤M≤106

输入样例:

3
aba
5
ababa

输出样例:

0 2

 感觉一个不错的视频【天勤考研】KMP算法易懂版_哔哩哔哩_bilibili

 模板代码

#include <iostream>

using namespace std;

const int N = 100010, M = 1000010;

int n, m;
int ne[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 = ne[j];
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;
    }

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

    return 0;
}
//来自y总

逐步解释

初始化变量

int m,n;
//主串长度,模式串长度
int ne[N]
//ne[N]是部分匹配表(也称为“前缀函数”)
int s[M],p[N]
//主串数组,模式串数组

主函数

cin >> n >> p+1 >> m >> s+1;

 为什么是p+1和s+1

  1. 从 1 开始的索引: 在许多竞赛题目或实现中,字符数组的下标往往从 1 开始,而不是从 0 开始。这是因为这样可以简化某些逻辑,比如处理字符串匹配问题时,下标从 1 开始可以减少下标的计算和调整,尤其是在循环或条件判断中。

  2. 输入偏移p + 1s + 1 将指针偏移到数组的第二个位置(即数组的下标为 1 的位置)。因此,当输入字符串时,实际存储的第一个字符会被放在 p[1]s[1] 中,而不是 p[0]s[0]

  3. 好处

    • 避免处理下标为 0 的特殊情况:如果字符串的下标从 1 开始,很多算法中的边界处理会更加简单,避免了处理下标为 0 的特殊情况。
    • 更加贴近竞赛习惯:在许多算法竞赛中,数组或字符串从 1 开始下标是比较常见的习惯,这样的代码风格使得处理问题更加直观。

示例

假设你有如下输入:

3
abc
7
abcabcabc

如果直接使用 ps 作为数组名(不加偏移),p[0] 会存储模式串的第一个字符 'a'p[1] 会存储 'b'p[2] 存储 'c'。但在这段代码中,加了一次偏移,p[1] 就存储了 'a'p[2] 存储 'b'p[3] 存储 'c'。这样,后续处理可以直接从 p[1] 开始,不用处理 p[0] 的边界问题。

部分匹配表的计算(前缀函数),求出ne[N]都为几(公共前后缀长度加一)

for (int i = 2, j = 0; i <= n; i ++ )
//这里我们从 i = 2 开始(注意 p 是从索引 1 开始的,而不是索引 0)。
//j 是模式串的前缀长度,初始值为 0。
//i 遍历模式串 p,范围从 2 到 n,用于计算每个位置的 ne[i]。
    {
        while (j && p[i] != p[j + 1]) j = ne[j];
//j && p[i] != p[j + 1]:
//j 表示当前匹配的前缀长度。如果 j == 0,表示没有可用的前缀,因此循环不会执行。
//如果 p[i] != p[j + 1],表示当前字符不匹配,需要通过 ne[j] 回退到前面可能的匹配位置。
//j = ne[j]:
//当不匹配时,回退 j 到当前最长前缀的长度,即 ne[j]。
        if (p[i] == p[j + 1]) j ++ ;
//如果当前字符 p[i] 和 p[j + 1] 匹配成功,增加 j 的值,表示匹配的前缀长度增加了。
        ne[i] = j;
//最终,ne[i] 被设置为当前的 j 值,即当前 i 位置的最长前缀后缀长度。
//这个值表示模式串 p 中 i 之前的子串中前缀和后缀相等的最大长度。
    }
举例解释

假设模式串 p = "ababca",构建 ne 数组:

  1. i = 2j = 0

    • p[2] = 'b'p[j + 1] = p[1] = 'a',不匹配,j = 0ne[2] = 0
  2. i = 3j = 0

    • p[3] = 'a'p[j + 1] = p[1] = 'a',匹配,j++ne[3] = 1
  3. i = 4j = 1

    • p[4] = 'b'p[j + 1] = p[2] = 'b',匹配,j++ne[4] = 2
  4. i = 5j = 2

    • p[5] = 'c'p[j + 1] = p[3] = 'a',不匹配,j = ne[2] = 0ne[5] = 0
  5. i = 6j = 0

    • p[6] = 'a'p[j + 1] = p[1] = 'a',匹配,j++ne[6] = 1

 

匹配主串与模式串

for (int i = 1, j = 0; i <= m; i ++ )
//个循环遍历文本 s 的每一个字符,从 i = 1 开始,到 i <= m 结束,m 是文本 s 的长度。
//j 表示当前模式串 p 中匹配的字符位置,初始化为 0。
    {
        while (j && s[i] != p[j + 1]) j = ne[j];
//如果当前 s[i] 和 p[j + 1] 不匹配,并且 j > 0,需要回退 j 到模式串的之前位置 ne[j],即当前 j 的//最长可复用前缀的结尾位置,尝试继续匹配。
//j && s[i] != p[j + 1]:
//当 j == 0 时,意味着无法回退,匹配失败,直接退出 while 循环。
//当 s[i] != p[j + 1] 时,表示当前字符不匹配,需要回退到 ne[j] 继续匹配。
//j = ne[j]:
//回退 j 到 ne[j] 处,尝试匹配下一个可能的前缀位置。
        if (s[i] == p[j + 1]) j ++ ;
//如果当前字符 s[i] 和模式串 p[j + 1] 匹配成功,j 向前移动一个位置,即 j++,表示模式串的匹配长度增加了。
        if (j == n)
        {
            printf("%d ", i - n);
            j = ne[j];
        }
//如果 j == n,表示模式串 p 的所有字符都成功匹配,找到了模式串在文本中的一个完整出现。
//输出 i - n,即模式串 p 在文本 s 中匹配的起始位置(因为 i 是当前匹配完成的位置,所以起始位置是 i- n)。
//然后,j = ne[j]:回退 j 到上一次匹配的最长公共前缀的位置,继续尝试匹配后续文本,可能会有重叠的匹配。
    }
例子

假设 s = "abababac"p = "abac",计算后的 next 数组为 [0, 0, 1, 2]

  1. i = 1, j = 0

    • s[1] = 'a'p[1] = 'a',匹配成功,j++,即 j = 1
  2. i = 2, j = 1

    • s[2] = 'b'p[2] = 'b',匹配成功,j++,即 j = 2
  3. i = 3, j = 2

    • s[3] = 'a'p[3] = 'a',匹配成功,j++,即 j = 3
  4. i = 4, j = 3

    • s[4] = 'b'p[4] = 'c',不匹配,j = ne[3] = 1,即回退到 p[2]
  5. 继续匹配

    • 重复这个过程直到找到完整的模式串匹配。
  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值