KMP学习笔记

前言

发明KMP的人真是神仙,前前后后学了1h+才懂。

算法详解

例题:Luogu P3375 【模板】KMP

KMP算法用来高效求解字符串匹配,现在需要在文本串寻找模式串的出现位置,在这里我们称文本串为 \(S\) ,模式串为 \(T\) ,并且 \(S\)\(T\) 的长度分别为 \(n\)\(m\)

首先定义 \(S_{l\dots r}\) 为字符串 \(S\) 的区间 \([l,r]\) 子串。
很容易想到暴力,依次枚举 \(S\) 中的每个位置 \(i\)\(T\) 中的每个位置 \(j\) ,若 \(S_i = T_i\) ,那么 \(i,j\) 移动到下一位,否则匹配失败,增加 \(i\) 并让 \(j\) 重新开始,若 \(j = m\) ,则当前的 \(i\) 即为答案。

可以发现暴力时间复杂度为 \(O(nm)\) ,太大的数据处理不了,怎么优化呢?我们发现 \(S_i \neq T_j\) 时的做法浪费了前面已经对比过的相等的字符,如果可以再次利用,就能减少时间复杂度。

1 2 3 4 5 6 7 8 9
S a b a b \(\textcolor{red}{a}\) b c a a
T a b a B \(\textcolor{red}{c}\)

如表,\(S_{1\dots 4} = T_{1\dots 4}\) ,但 \(S_5 \neq T_5\) ,若从 \(S_{1\dots 4}\) 的前面和 \(T_{1\dots 4}\) 的后面去掉 \(1\) 个字符,那么他们不相等;若去掉 \(2\) 个字符,那么此时他们相等,如果让 \(j\)\(3\) 开始,就避免了比较前面相等的字符,接着比较 \(S,T\) 的剩余部分就行了。

可以发现,\(j\) 跳过比较了一个相等子串 ab,而它刚好是 \(S_{1\dots 4}\) 的后缀和 \(T_{1\dots 4}\) 的前缀,再举一个例子,\(S=\) aabaabaab\(T=\) aabaabaaa,不相等时前面有多个相等前后缀,举出两个:

1 2 3 4 5 6 7 8 9
S a a b \(\textcolor{blue}{a}\) \(\textcolor{blue}{a}\) \(\textcolor{blue}{b}\) \(\textcolor{blue}{a}\) \(\textcolor{blue}{a}\) \(\textcolor{red}{b}\)
T \(\textcolor{blue}{a}\) \(\textcolor{blue}{a}\) \(\textcolor{blue}{b}\) \(\textcolor{blue}{a}\) \(\textcolor{blue}{a}\) b a a \(\textcolor{red}{a}\)
1 2 3 4 5 6 7 8 9
S a a b a a b \(\textcolor{blue}{a}\) \(\textcolor{blue}{a}\) \(\textcolor{red}{b}\)
T \(\textcolor{blue}{a}\) \(\textcolor{blue}{a}\) b a a b a a \(\textcolor{red}{a}\)

显然,取表一中的前后缀,最后找到的答案更靠前,可以不重不漏地枚举完所有答案,所以应取最长相等前后缀,另外,因为 \(S_{1\dots 8} = T_{1\dots 8}\) ,所以可以转化为计算 \(T\)最长公共前后缀,可以给 \(j\) 打一张表 \(next\)\(next_j\) 表示 \(T_{1\dots j}\) 的最长公共前后缀,注意这里是真前后缀,即不包括字符串本身,否则没有意义),匹配失败时就跳到 \(next_j\) 继续比较即可,这里的 \(next\) 就是KMP算法中的部分匹配表(Partial Match Table,简称PMT)。

我们已经知道如何枚举了,那么如何计算 \(next\) 呢?若 \(T=\) aabaabaaaa,打出下表:

j 1 2 3 4 5 6 7 8 next
T a b a c a b a b
1 a 0
2 a b 0
3 a b a 1
4 a b a c 0
5 a b a c a 1
6 a b a c a b 2
7 a b a c a b a 3
8 \(\textcolor{blue}{a}\) \(\textcolor{blue}{b}\) \(\textcolor{blue}{a}\) \(\textcolor{red}{c}\) \(\textcolor{blue}{a}\) \(\textcolor{blue}{b}\) \(\textcolor{blue}{a}\) \(\textcolor{red}{b}\) 2

已知 \(next_1=0\) ,当 \(j \ge 2\) 时,分类讨论:

  1. \(T_{next_{j-1} + 1} = T_j\) ,即去掉当前字符的最长公共前后缀前缀的下一个字符等于后缀的下一个字符(当前字符),\(next_j = next_{j-1}+1\)

  2. \(T_{next_{j-1} + 1} \neq T_j\) ,还能从以前的状态推导吗?当 \(j = 8\) 时,发现如果前面的蓝色子串的前缀和后面的蓝色子串的后缀相等,就可以继续往后面比较从而避免不必要的操作,我们从前面的计算得出这两个子串是相等的,也就是说前面这部分的后缀等于后面这部分的后缀,那我们转化为求前面这部分的最长公共前后缀即可,前面的前后缀我们已经计算过了,为 \(next_{next_{j-1}}\) ,接下来我们再重复操作 \(1,2\) 就行了。

代码

这里方便起见,\(next\)\(0\) 开始,和字符串下标对应,写代码时注意数组下标对应就行了。

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
int nxt[N];

int main(){
    string s, t;
    cin >> s >> t;
    int n = s.size(), m = t.size();
    for(int i = 1, j = 0; i < m; ++i){
        while(j && t[i] != t[j]) j = nxt[j - 1];
        if(t[i] == t[j]) ++j;
        nxt[i] = j;
    }
    for(int i = 0, j = 0; i < n; ++i){
        while(j && s[i] != t[j]) j = nxt[j - 1];
        if(s[i] == t[j]) ++j;
        if(j == m){
            cout << i - m + 2 << '\n';
            j = nxt[j - 1];
        }
    }
    for(int i = 0; i < m; ++i) cout << nxt[i] << ' ';
    return 0;
}
posted @ 2025-09-27 18:30  EvanYow  阅读(17)  评论(0)    收藏  举报