KMP 算法
💡 核心思想
KMP(Knuth-Morris-Pratt)算法用于在文本串中查找模式串的所有出现位置。核心是预处理 next 数组(前缀函数),表示模式串中每个位置的最长相等真前后缀长度。匹配失败时利用 next 数组跳过已匹配的部分,避免回溯。
KMP 的难点在于理解 next 数组的含义。$next[i]$ 表示 $s[0..i]$ 这个子串中,最长的"既是前缀又是后缀"的真子串长度。比如 "ababa" 的 next[4] = 3("aba" 既是前缀又是后缀)。next 数组本身可以用 KMP 的思想来求解——"用自己匹配自己"。
🎯 直觉理解
想象你在书里找一个长句子。朴素方法是逐个位置对齐比对,失败了从头开始。KMP 的聪明之处是:如果你已经匹配了 "abcabc" 然后失败了,你不需要从头开始——因为你知道后面三个字符是 "abc",它正好也是模式串的开头!所以可以直接跳到匹配 "abc" 的状态继续。
📝 算法流程
- 求 next 数组:j = next[j-1] 回退找匹配
- 匹配过程:失配时 j = next[j-1] 跳转
- next[i] = s[0..i] 的最长公共前后缀长度
- 时间复杂度 $O(n+m)$
$$next[i] = \max\{k : s[0..k-1] = s[i-k+1..i]\}$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | $O(n+m)$(预处理 $O(m)$ + 匹配 $O(n)$) |
| 空间 | $O(m)$(next 数组) |
💻 参考实现(C++)
C++ (C++17)
#include <bits/stdc++.h>
using namespace std;
int main() {
string text, pat; cin >> text >> pat;
int n = text.size(), m = pat.size();
// 求 next 数组
vector<int> nxt(m, 0);
for (int i = 1; i < m; i++) {
int j = nxt[i-1];
while (j > 0 && pat[i] != pat[j]) j = nxt[j-1];
if (pat[i] == pat[j]) j++;
nxt[i] = j;
}
// 匹配
vector<int> ans;
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && text[i] != pat[j]) j = nxt[j-1];
if (text[i] == pat[j]) j++;
if (j == m) { ans.push_back(i - m + 2); j = nxt[j-1]; }
}
for (int x : ans) cout << x << "\n";
for (int x : nxt) cout << x << " ";
return 0;
}⚠️ 常见坑点
next 数组含义有两种定义方式(是否整体右移)要统一
求 next 和匹配的代码结构几乎一样,但意义不同
匹配成功后 j = nxt[j-1] 继续搜索
下标从 0 还是 1 开始要统一
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P3375 KMP字符串匹配 | 洛谷 | CSP-S | KMP模板 |
| P4391 无线传输 | 洛谷 | CSP-S | next数组应用 |