门情提要,参考资料:oi wiki前缀函数与 KMP 算法 - OI Wiki.
资料视频链接:最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili.
看完文章了解KMP,直接干碎NOIP!
一、基本概念
KMP算法(Knuth-Morris-Pratt算法)是一种高效的字符串匹配算法,用于在一个主字符串(文本串)中查找模式串(子串)的出现位置。与暴力匹配算法相比,KMP算法通过预处理模式串,避免了不必要的字符比较,从而将时间复杂度降低到线性级别(O(n + m),其中n是主字符串长度,m是模式串长度)。
二、核心思想
在字符串匹配中,暴力算法会在每次失配后将模式串右移一位,重新开始匹配,进而导致大量重复计算。而KMP算法则通过预处理字符串来构建一个部分匹配表(Partial Match Table,简称PMT),记录模式串中每个位置之前的子串的最长相等前后缀长度。利用这个表,KMP算法可以在失配时跳过已知的匹配部分,从而提高匹配效率。
关键点:
1.最长相等前后缀:对于模式串的一个子串,其最长相等前后缀是指最长的相同前缀和后缀。
2.部分匹配表(PMT):也称为next
数组,记录每个位置的最长相等前后缀长度。
3.失配时的跳转:当匹配失败时,根据next
数组直接跳到下一个可能的匹配位置,而不是逐个字符移动。
三、部分匹配表(PMT)的构建
部分匹配表为KMP算法的关键。本质为记录模式串中每个位置的最长相等前后缀长度。(其可将KMP的时间复杂度由 O(n * m) 降到 O(n + m))
其构建过程如下:
1.初始化 next[0] = 0 (因为单个字符串没有前后缀) 。
2.对于每个位置 i ,通过比较当前字符和已知的最长相等前后缀的下一个字符,逐步扩展到最长相等前后缀。
3.如果匹配成功,则 next[i] = next[i-1] + 1 ;如果匹配失败,则回退到更短的相等前后缀。
ed:
给定一个模式串:"abababca" ;
i | 字符 | 最长相等前后缀 | next[i] |
0 | a | - | 0 |
1 | b | - | 0 |
2 | a | a | 1 |
3 | b | ab | 2 |
4 | a | aba | 1 |
5 | b | abab | 2 |
6 | c | - | 0 |
7 | a | - | 0 |
以下是匹配表的具体解释:
1) 部分匹配表的作用
在KMP算法中,部分匹配表的作用是在失配时快速跳过已知匹配的部分。具体来说:
-
当模式串与主字符串匹配失败时,
next
数组可以告诉我们模式串可以跳过的最大长度。 -
通过
next
数组,可以直接跳到下一个可能的匹配位置,而不需要从头开始匹配。
例如,在模式串"abababca"
中:
-
如果匹配到位置
i = 5
(字符b
)时失配,next[5] = 2
,表示最长相等前后缀是"ab"
。 -
因此,模式串可以直接跳到
j = 2
的位置继续匹配,跳过了已知匹配的部分"ab"
。
2) 部分匹配表的计算方法
计算部分匹配表的过程是一个动态规划的过程。以下是计算next
数组的步骤:
-
初始化:
-
next[0] = 0
,因为单个字符没有前后缀。 -
j = 0
,表示当前已匹配的最长相等前后缀长度。
-
-
遍历模式串:
-
对于每个位置
i
(从1开始),尝试将当前字符pattern[i]
与已匹配的最长相等前后缀的下一个字符pattern[j]
进行比较。 -
如果匹配成功(
pattern[i] == pattern[j]
),则j++
,并设置next[i] = j
。 -
如果匹配失败(
pattern[i] != pattern[j]
),则回退到更短的相等前后缀,即j = next[j - 1]
,直到找到匹配或j == 0
。
-
-
更新
next
数组:-
每次成功匹配后,更新
next[i] = j
。
-
以下是计算部分匹配表的代码实现:
cpp复制
vector<int> computePrefixFunction(const string& pattern) {
int m = pattern.length();
vector<int> next(m, 0); // 初始化next数组
int j = 0; // j表示当前已匹配的最长相等前后缀长度
for (int i = 1; i < m; i++) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1]; // 失配时回退到更短的相等前后缀
}
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j; // 更新next数组
}
return next;
}
3) 部分匹配表的示例
以模式串"abababca"
为例,逐步计算next
数组:
-
初始化:
-
next[0] = 0
,j = 0
。
-
-
计算
next[1]
:-
i = 1
,pattern[1] = 'b'
,j = 0
。 -
pattern[1] != pattern[0]
,j
保持为0。 -
next[1] = 0
。
-
-
计算
next[2]
:-
i = 2
,pattern[2] = 'a'
,j = 0
。 -
pattern[2] == pattern[0]
,j++
。 -
next[2] = 1
。
-
-
计算
next[3]
:-
i = 3
,pattern[3] = 'b'
,j = 1
。 -
pattern[3] == pattern[1]
,j++
。 -
next[3] = 2
。
-
-
计算
next[4]
:-
i = 4
,pattern[4] = 'a'
,j = 2
。 -
pattern[4] != pattern[2]
,j = next[1] = 0
。 -
pattern[4] == pattern[0]
,j++
。 -
next[4] = 1
。
-
-
计算
next[5]
:-
i = 5
,pattern[5] = 'b'
,j = 1
。 -
pattern[5] == pattern[1]
,j++
。 -
next[5] = 2
。
-
-
计算
next[6]
:-
i = 6
,pattern[6] = 'c'
,j = 2
。 -
pattern[6] != pattern[2]
,j = next[1] = 0
。 -
pattern[6] != pattern[0]
,j
保持为0。 -
next[6] = 0
。
-
-
计算
next[7]
:-
i = 7
,pattern[7] = 'a'
,j = 0
。 -
pattern[7] == pattern[0]
,j++
。 -
next[7] = 1
。
-
最终,next
数组为:[0, 0, 1, 2, 1, 2, 0, 1]
。
4) 部分匹配表的直观理解
部分匹配表的核心是利用模式串的自相似性。通过记录每个位置的最长相等前后缀长度,KMP算法可以在失配时跳过已知匹配的部分,而不是从头开始匹配。
例如:
-
模式串
"abab"
的next
数组为[0, 0, 1, 2]
。 -
如果匹配到
i = 3
时失配,next[3] = 2
,表示最长相等前后缀是"ab"
,可以直接跳到j = 2
的位置继续匹配。
四、KMP算法的具体实现
1.构建部分匹配表
vector<int> computePrefixFunction(const string& pattern) {
int m = pattern.length();
vector<int> next(m, 0); // next数组
int j = 0; // j表示当前已匹配的最长相等前后缀长度
for (int i = 1; i < m; i++) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1]; // 回退到更短的相等前后缀
}
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j;
}
return next;
}
2.字符串匹配过程
vector<int> kmpSearch(const string& text, const string& pattern) {
int n = text.length();
int m = pattern.length();
vector<int> next = computePrefixFunction(pattern);
vector<int> matches; // 存储匹配位置
int j = 0; // j表示当前匹配到模式串的位置
for (int i = 0; i < n; i++) {
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1]; // 失配时,利用next数组跳过已知匹配部分
}
if (text[i] == pattern[j]) {
j++;
}
if (j == m) { // 匹配成功
matches.push_back(i - m + 1); // 记录匹配位置
j = next[j - 1]; // 继续查找下一个匹配
}
}
return matches;
}
3.完整代码示例
#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector<int> computePrefixFunction(const string& pattern) {
int m = pattern.length();
vector<int> next(m, 0);
int j = 0;
for (int i = 1; i < m; i++) {
while (j > 0 && pattern[i] != pattern[j]) {
j = next[j - 1];
}
if (pattern[i] == pattern[j]) {
j++;
}
next[i] = j;
}
return next;
}
vector<int> kmpSearch(const string& text, const string& pattern) {
int n = text.length();
int m = pattern.length();
vector<int> next = computePrefixFunction(pattern);
vector<int> matches;
int j = 0;
for (int i = 0; i < n; i++) {
while (j > 0 && text[i] != pattern[j]) {
j = next[j - 1];
}
if (text[i] == pattern[j]) {
j++;
}
if (j == m) {
matches.push_back(i - m + 1);
j = next[j - 1];
}
}
return matches;
}
int main() {
string text = "abababca";
string pattern = "abab";
vector<int> matches = kmpSearch(text, pattern);
if (matches.empty()) {
cout << "Pattern not found" << endl;
} else {
cout << "Pattern found at positions: ";
for (int pos : matches) {
cout << pos << " ";
}
cout << endl;
}
return 0;
}
五、KMP算法的关键点
1.部分匹配表的作用
° 部分匹配表记录了模式串中每个位置的最长相等前后缀长度。
° 在失配时,利用next
数组可以快速跳过已知匹配部分,避免重复比较。
2.失配时的跳转
° 当text[i] != pattern[j]
时,j = next[j - 1]
,即回退到更短的相等前后缀。
° 如果j == 0
时仍然失配,则模式串向右移动一 位。
3.匹配成功的处理
° 当j == m
时,表示模式串完全匹配,记录匹配位置。
° 匹配成功后,j = next[j - 1]
,继续查找下一个匹配。
六、KMP算法时间复杂度
1.构建部分匹配表:O(m),其中m是模式串的长度。
2.字符串匹配过程:O(n),其中n是主字符串的长度。
3.总时间复杂度:O(n + m),线性级别。
当然我们并不排除某些题中的询问或者其他操作,KMP的时间复杂度有可能是O((n + x) m)。
七、注意事项
1.部分匹配表的边界条件:
° 当j == 0
且失配时,模式串直接向右移动一位。
° 部分匹配表的构建需要特别注意边界条件,避免数组越界。
2.模式串为空:
° 如果模式串为空,需要特殊处理,避免逻辑错误。
3.多模式匹配:
° KMP算法主要用于单模式匹配。如果需要匹配多个模式串,可以使用Aho-Corasick算法等扩展方法。
八、总结
KMP算法是一种高效的字符串匹配算法,通过预处理模式串构建部分匹配表,避免了暴力匹配中的重复计算。它的时间复杂度为线性级别(O(n + m))(不排除特殊的 O((n + x) m) 情况),适用于大规模文本匹配场景。理解KMP算法的关键在于掌握部分匹配表的构建和失配时的跳转机制。
课下习题:P3375 【模板】KMP - 洛谷
Code ED:
//KMP
//时间复杂度:O(n)
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#define ll long long
using namespace std;
const int N = 1e6+10;
ll kmp[N];
ll len_a,len_b;//定义a,b两字符串长度
ll ace;
char a[N],b[N];//a,b字符串
int main(void){
scanf("%s", a+1);
scanf("%s", b+1);
len_a = strlen(a+1);
len_b = strlen(b+1);
//优先处理b数组,之后映射到a数组
for(int i = 2; i <= len_b; ++i){
while(ace && b[i]!=b[ace+1])//匹配失败,会跳,直到成功匹配
ace = kmp[ace];
if(b[ace+1] == b[i]) ace++;
kmp[i] = ace;
}
ace = 0;
//处理a数组,替换替换部分代码就好
for(int i = 1; i <= len_a; ++i){
while(ace > 0 && b[ace+1] != a[i])
ace = kmp[ace];
if(b[ace+1] == a[i])
ace++;
if(ace == len_b) {cout << i-len_b+1 << endl; ace = kmp[ace];}//匹配成功,输出
}
for(int i = 1; i <= len_b; ++i) cout << kmp[i] << " ";
return 0;
}