数据结构与算法学习笔记----KMP
@@ author: 明月清了个风
@@ last edited: 2024.11.24
Acwing 831. KMP字符串
给定一个字符串 S S S,以及一个模式串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P P P在字符串 S S S中多次作为子串出现。
求出模式串 P P P在字符串 S S S中所有出现的位置的起始下标。
输入格式
第一行包含整数 N N N,表示字符串 P P P的长度。
第二行输入字符串 P P P。
第三行输入整数 M M M,表示字符串 S S S的长度。
第四行输入字符串 S S S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 0 0开始计数),整数之间用空格隔开。
数据范围
1 ≤ N ≤ 1 0 5 1 \leq N \leq 10^{5} 1≤N≤105,
1 ≤ M ≤ 1 0 6 1 \leq M \leq 10^{6} 1≤M≤106
思路
首先考虑暴力做法,两重循环即可完成,伪代码如下:
... // 原字符串为S,模式串为Pfor(int i = 0; i < n; i ++) // 循环字符串S{bool flag = false;int temp = i; // 存这一轮匹配的起始位置for(int j = 0; j < m;) // 循环模式串P{while(s[i] == p[j] && j < m) i ++, j ++;if(j == m) flag = true; // 将模式串P循环到底说明这轮匹配成功else break; // 否则表示这一轮匹配失败,直接跳出。}if(flag) cout << temp << ' ';i = temp; }
...
时间复杂度为:O(n * m)
这里模拟KMP的暴力匹配过程,字符串 S S S和 P P P中的元素分别用 a i a_{i} ai和 b i b_{i} bi表示:
-
假设目前已经匹配完成的是 a i a_{i} ai和 b j b_{j} bj,也就是说字符串 S S S中的
[1 ~ i]
与模式串 P P P中的[1 ~ j]
相等,即图中红框中的两部分相等
-
当 a i + 1 a_{i+1} ai+1与 b j + 1 b_{j + 1} bj+1不相等时,此时匹配失败(如图中紫色虚线框中),那么此时需要将模式串 P P P向后移动,暴力做法中应移动一位,若此时字符串 S S S的
[2 ~ i]
项与模式串 P P P的[1 ~ j - 1]
项能够匹配成功也就意味着,对于模式串 P P P而言,其前[1 ~ j]
项中前缀[1 ~ j - 1]
项与后缀[2 ~ j]
项是相等的(由一步的图中可以知道字符串 S S S中的[2 ~ i]
项等于模式串 P P P中的[2 ~ j]
项)
那么如果对于模式串而言,若其前 [1 ~ j]
项中前缀[1 ~ j - 1]
项与后缀[2 ~ j]
项不相等,相等的是前缀[1 ~ j - 2]
与后缀[3 ~ j]
,那么就会出现下图的情况:
当暴力匹配第一步匹配到 a i + 1 a_{i+1} ai+1失败后,试图将模式串向后移动一位重新匹配,但是肯定匹配未到 a i + 1 a_{i+1} ai+1就会失败(因为其前 [1 ~ j]
项中前缀[1 ~ j - 1]
项与后缀[2 ~ j]
项不相等),因此继续向后移动一位,因为此时模式串 P P P中前缀[1 ~ j - 2]
与后缀[3 ~ j]
相等,因此可以直接继续匹配 a i + 1 a_{i+1} ai+1,图中所示的第二步就是可被优化掉的操作,也就是跳过一定不可能匹配成功的位置。
综上所述可以得出优化暴力做法的思路:针对模式串 P P P预处理出其以第一项为头的所有长度的连续子串的最大的相同前后缀子串。
解释一下这句话:
- 因为模式串 P P P需要完整匹配,因此总是需要从第一项开始匹配
- 当匹配到某一个位置失败后,因为前面的都已与字符串 S S S匹配成功,且被匹配字符串 S S S不动,那么此时将模式串 P P P向后移动就相当于用模式串 P P P去匹配自己的后缀,那么为了减少匹配次数,假设匹配失败位置是 b i b_{i} bi,那么只要知道模式串 P P P的以第一项 b 1 b_{1} b1为头到 b i − 1 b_{i-1} bi−1这个子串(即子串 b 1 ∼ i − 1 b_{1 \sim i-1} b1∼i−1)的最大相同前后缀即可直接继续匹配 b i b_{i} bi,若仍不匹配,继续递归即可。
优化后的KMP算法时间复杂度为O(m + n)
代码
#include <iostream>using namespace std;const int N = 100010, M = 1000010;int n, m;
char s[M], p[N];
int ne[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){cout << i - n << ' ';j = ne[j];}}return 0;
}