KMP
创建日期:2025-01-09
KMP基本原理和实现
KMP的作用是在一个字符串 \(s\) 中查找另一个字符串 \(t\),我们将 \(s\) 称为文本串,\(t\) 称为模式串。
不妨令 \(n=|s|,m=|t|\)。
在朴素算法中,我们需要 \(O(n)\) 枚举模式串在文本串中的起始位置 \(i\),再 \(O(m)\) 检查模式串的每一个字符 \(t_j\) 是否与文本串 \(s_j\) 相比配。
可以发现,在检查过程中,每当有一位匹配不上时,就可以跳过并枚举检查下一个模式串出现位置,故常规情况下,算法复杂度到不了 \(O(n \times m)\)。但即便如此,在精心构造的数据下,仍能使算法复杂度达到 \(O(n \times m)\)。
例如以下数据:
\(s=aaaaaaaaaaaaab\)
\(t=aaaaab\)
在匹配过程中,每次都会检查到模式串的最后一位,且几乎每次都会匹配失败,故复杂度达到 \(O(n \times m)\)。
可以发现,复杂度如此高的原因是在每次失败后 \(j\) 直接回到了1,浪费了大量重复时间。正是因为如此,才需要KMP。
KMP的步骤如下:
设置指针 \(i\) 与指针 \(j\),\(i\) 表示现在正在检查 \(s\) 中的第 \(i\) 位与 \(t\) 中的第 \(j\) 位。
- 当 \(s_i\) 等于 \(t_j\) 时,将 \(i\)++, \(j\)++,继续检查。
- 当 \(s_i\) 不等于 \(t_j\) 时,将 \(j\) 赋值为 \(t\) 以 \(j\) 结尾的border的长度(字符串下表从0开始)。
那border长度数组怎么求呢?
令以 \(k\) 结尾的border长度为 \(nxt_k\)。
设置指针 \(i\),\(j\) 表示正在检查 \(t\) 中以 \(i\) 结尾的后缀与以 \(j\) 结尾的前缀
- 当 \(s_i\) 等于 \(s_j\) 时,将 \(i\)++,\(j\)++并将 \(nxt_j\)设为 \(j\),继续检查。
- 当 \(s_i\) 不等于 \(s_j\) 时,将 \(j\) 赋值为 \(nxt_j\)。
这个过程与之前匹配的过程很相似,代码也差不多。
模版代码如下:
#include <bits/stdc++.h>
using namespace std;
const int LEN = 1e6 + 50;
string s, t;
int nxt[LEN];
void get_next() {
nxt[0] = -1;
int i = 0, j = -1;
while (i < t.size()) {
if (j == -1 || t[i] == t[j])
nxt[++i] = ++j;
else
j = nxt[j];
}
}
void KMP() {
get_next();
int i = 0, j = 0;
while (i < s.size()) {
if (j == t.size() - 1 && s[i] == t[j]) {
cout << i - t.size() + 2 << '\n';
j = nxt[j];
}
if (j == -1 || s[i] == t[j])
i++, j++;
else
j = nxt[j];
}
}
int main() {
cin >> s >> t;
KMP();
for (int i = 0; i < t.size(); i++)
cout << nxt[i + 1] << ' ';
return 0;
}
KMP常用的结论
1.最短周期
题目概述:见洛谷P4391
结论:最短周期长度等于 \(|s|-nxt_{|s|}\)。
2.最长周期
题目概述:见洛谷P3435
结论:
令
则最长周期长度等于 \(|s|-f(x)\) 。
注意:在实际编写代码的过程中,可以再 \(f(x)\) 函数中使用类似并查集的路径压缩优化。

浙公网安备 33010602011771号