算法学习笔记(16)——KMP

KMP

KMP 算法,又称模式匹配算法,能够在线性时间内判定字符串 \(P[1 \sim N]\) 是否为字符串 \(S[1 \sim M]\) 的子串。并求出字符串 \(P\) 在字符串 \(S\) 中各次出现的位置。

首先,一个 \(O(NM)\) 的朴素做法是,尝试枚举字符串 \(S\) 中的每个位置 \(i\),把字符串 \(P\) 与字符串 \(S\) 的后缀 \(S[i \sim M]\) 对齐,向后扫描逐一比较 \(P[1]\)\(S[i]\)\(P[2]\)\(S[i+1]\),...... 是否相等。我们把这种比较过程称为 \(P\)\(S\) 尝试进行“匹配”。

KMP算法能够更高效、更准确地处理这个问题,并且能够为我们提供一些额外的信息。详细地讲,KMP算法分为两步。

  1. 对字符串 \(P\) 进行自我“匹配”,求出一个数组 \(next\),其中 \(next[i]\) 表示“\(P\) 中以 \(i\) 结尾的非前缀子串”与“\(P\) 的前缀”能够匹配的最长长度,即:

    \[next[i] = \max\lbrace j \rbrace, 其中 j < i 并且 P[i-j+1 \sim i]=P[1 \sim j] \]

    特别的,当不存在这样的 \(j\) 时,令 \(next[j]=0\)
  2. 对字符串 \(P\)\(S\) 进行匹配,求出一个数组 \(f\),其中 \(f[i]\) 表示“ \(S\) 中以 \(i\) 结尾的子串”与“\(P\) 的前缀”能够匹配的最大长度,即:

    \[f[i] = \max\lbrace j \rbrace, 其中 j \le i 并且 S[i-j+1 \sim i] = P[1 \sim j] \]

下面介绍 \(next\) 数组的求法:

1. 初始化 next[1] = j = 0,假设 next[1 ~ i-1] 已求出,下面求解 next[i]
2. 不断尝试扩展匹配长度 j,如果扩展失败(下一个字符不相等),令 j 变为 next[j],直至 j 为0(应该重新从头开始匹配)
3. 如果能够扩展成功,匹配长度 j 就增加 1。next[i] 的值就是 j。

求解 \(f\) 与求解 \(next\) 的过程基本一致,具体参考模板代码。

题目链接:AcWing 831. KMP字符串

#include <iostream>

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

int n, m;
char p[N];  // 模版串
char s[M];  // 模式串
int ne[N];  // next数组

int main()
{
    // 注意,此处从下标1开始读入字符串,方便后续操作
    cin >> n >> p + 1 >> m >> s + 1;
    
    // 1. 预处理next数组,默认next[1] = 0,从2开始处理
    for (int i = 2, j = 0; i <= n; i ++ ) {
        // 当j不是第一个匹配点且匹配不成功时,从下一候选项尝试
        while (j && p[i] != p[j + 1]) j = ne[j];
        // 匹配成功
        if (p[i] == p[j + 1]) j ++;
        ne[i] = j;
    }
    
    // 2. 匹配
    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 ++;
        // 此时就是 P 在 S 中的某一次出现
        if (j == n) {
            // 输出匹配成功的起始位置
            cout << i - n << ' ';
            // 从下一候选项继续尝试匹配
            j = ne[j];
        }
    }
    
    return 0;
}

在上面代码的while循环中,j的值不断减小,j=ne[j]的执行次数不会超过每层for循环开始时j的值与while循环结束时j的值之差。而在每层for循环中,j的值至多增加 \(1\)。因为j始终非负,所以在整个计算过程中,j减小的幅度总和不会超过j增加的幅度总和。故j的总变化次数至多为 \(2(N+M)\)。整个算法的时间复杂度为 \(O(N+M)\)。这样只需遍历一次 \(S\) 就可以得到 \(P\) 的所有匹配位置。

  • 计算Partial_Table(或者说是计算模式串的最长公共前缀后缀长度列表)时的比较次数介于[m,2m],假设m是模式串的长度.
  • 比较模式串和子串时比较次数介于[n,2n],最坏情形形如T="aaaabaaaab",P="aaaaa".
posted @ 2022-12-09 22:06  S!no  阅读(37)  评论(0编辑  收藏  举报