题解:AcWing 831 KMP字符串
【题目来源】
AcWing:831 KMP字符串 - AcWing题库
【题目描述】
给定一个字符串 \(S\),以及一个模式串 \(P\),所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 \(P\) 在字符串 \(S\) 中多次作为子串出现。
求出模式串 \(P\) 在字符串 \(S\) 中所有出现的位置的起始下标。
【输入】
第一行输入整数 \(N\),表示字符串 \(P\) 的长度。
第二行输入字符串 \(P\)。
第三行输入整数 \(M\),表示字符串 \(S\) 的长度。
第四行输入字符串 \(S\)。
【输出】
共一行,输出所有出现位置的起始下标(下标从 \(0\) 开始计数),整数之间用空格隔开。
【输入样例】
3
aba
5
ababa
【输出样例】
0 2
【核心思想】
-
问题分析:给定长度为 \(N\) 的模式串 \(P\) 和长度为 \(M\) 的文本串 \(S\),要求找出 \(P\) 在 \(S\) 中所有出现位置的起始下标。暴力匹配最坏需要 \(O(N \cdot M)\) 时间,且每次失配后文本串指针回溯导致大量重复比较。KMP 算法通过预处理
next数组,使得文本串指针永不回退,在 \(O(N + M)\) 时间内完成所有匹配。 -
算法选择:
- KMP(Knuth-Morris-Pratt)算法:利用模式串自身的结构信息,预处理
next数组记录每个前缀的最长相等真前缀与真后缀长度,失配时模式串指针智能回退而非从头开始 - 多模式匹配扩展:与仅查找首次出现不同,本题需要在每次完全匹配后,令 \(j = \text{next}[j]\) 继续寻找下一个匹配位置,保证不遗漏重叠出现
- KMP(Knuth-Morris-Pratt)算法:利用模式串自身的结构信息,预处理
-
关键步骤:
- 读取输入:模式串长度 \(N\)、模式串 \(P\)、文本串长度 \(M\)、文本串 \(S\)(下标从 \(1\) 开始存储)
- 预处理 next 数组(\(O(N)\)):
- 初始化 \(j = 0\),枚举 \(i\) 从 \(2\) 到 \(N\)
while (j && p[i] != p[j+1]) j = ne[j]:当前后缀不匹配时,\(j\) 回退到 next 值if (p[i] == p[j+1]) j++:匹配成功则扩展前后缀ne[i] = j:记录 \(P[1..i]\) 的最长相等前后缀长度
- KMP 匹配过程(\(O(M)\)):
- 初始化 \(j = 0\),枚举文本串指针 \(i\) 从 \(1\) 到 \(M\)
- 失配时回退:
while (j && s[i] != p[j+1]) j = ne[j] - 匹配时前进:
if (s[i] == p[j+1]) j++ - 完全匹配处理:当 \(j == N\) 时,\(P\) 在 \(S\) 中出现,起始位置为 \(i - N\)(因下标从 \(0\) 开始输出)。随后令 \(j = \text{ne}[j]\),继续匹配下一个可能位置(处理重叠情况,如 \(P = \text{"aba"}\) 在 \(S = \text{"ababa"}\) 中重叠出现)
- 输出所有起始位置
-
时间/空间复杂度:
- 时间复杂度:\(O(N + M)\)。预处理 next 数组 \(O(N)\),匹配过程 \(O(M)\),每个字符最多被比较常数次
- 空间复杂度:\(O(N)\),存储 next 数组
-
KMP 与 next 数组的核心思想:
- next 数组的本质:\(\text{next}[i]\) 表示子串 \(P[1..i]\) 的最长相等真前缀与真后缀的长度。当 \(P[j+1]\) 与 \(S[i]\) 失配时,由于 \(P[1..j]\) 已匹配,而 \(P[1..\text{next}[j]]\) 与 \(P[j-\text{next}[j]+1..j]\) 相同,因此可以直接将 \(j\) 回退到 \(\text{next}[j]\),无需移动文本串指针
- 文本串指针不回退:KMP 的核心优势在于 \(i\) 始终只增不减。每次失配时,利用已匹配部分的信息"跳过"不可能匹配的位置,而非像暴力算法那样让 \(i\) 回溯
- 重叠匹配的处理:匹配成功后令 \(j = \text{next}[j]\) 而非 \(j = 0\),是为了处理模式串自身有重叠结构的情况。例如 \(P = \text{"aba"}\),匹配 "ababa" 时,第一个匹配在位置 \(0\),\(\text{next}[3] = 1\),回退后 \(j = 1\) 意味着已匹配前缀 "a",可直接尝试从位置 \(2\) 开始下一次匹配
- 自身匹配构建 next:next 数组的构建过程本质上是模式串 \(P\) 与自身的 KMP 匹配,利用了相同的回退逻辑
- 适用于:单模式串多匹配、字符串查找、循环节判断、字符串压缩等
【解题思路】

【算法标签】
KMP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100010, M = 1000010; // 定义常量N和M,分别表示模式串和文本串的最大长度
int n, m; // n: 模式串长度,m: 文本串长度
char p[N], s[M]; // p: 模式串,s: 文本串
int ne[N]; // ne: next数组,用于KMP算法
int main() {
cin >> n >> p + 1 >> m >> s + 1; // 输入模式串和文本串(从下标1开始存储)
// KMP算法预处理next数组
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++; // 匹配时j前进
ne[i] = j; // 更新next数组
}
// KMP匹配过程
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++; // 匹配时j前进
if (j == n) { // 完全匹配
printf("%d ", i - n); // 输出匹配的起始位置
j = ne[j]; // 回退,继续匹配
}
}
return 0;
}
【运行结果】
3
aba
5
ababa
0 2
浙公网安备 33010602011771号