算法学习笔记(16)——KMP
KMP
KMP 算法,又称模式匹配算法,能够在线性时间内判定字符串 \(P[1 \sim N]\) 是否为字符串 \(S[1 \sim M]\) 的子串。并求出字符串 \(P\) 在字符串 \(S\) 中各次出现的位置。
首先,一个 \(O(NM)\) 的朴素做法是,尝试枚举字符串 \(S\) 中的每个位置 \(i\),把字符串 \(P\) 与字符串 \(S\) 的后缀 \(S[i \sim M]\) 对齐,向后扫描逐一比较 \(P[1]\) 与 \(S[i]\), \(P[2]\) 与 \(S[i+1]\),...... 是否相等。我们把这种比较过程称为 \(P\) 与 \(S\) 尝试进行“匹配”。
KMP算法能够更高效、更准确地处理这个问题,并且能够为我们提供一些额外的信息。详细地讲,KMP算法分为两步。
- 对字符串 \(P\) 进行自我“匹配”,求出一个数组 \(next\),其中 \(next[i]\) 表示“\(P\) 中以 \(i\) 结尾的非前缀子串”与“\(P\) 的前缀”能够匹配的最长长度,即:\[next[i] = \max\lbrace j \rbrace, 其中 j < i 并且 P[i-j+1 \sim i]=P[1 \sim j] \]特别的,当不存在这样的 \(j\) 时,令 \(next[j]=0\)。
- 对字符串 \(P\) 与 \(S\) 进行匹配,求出一个数组 \(f\),其中 \(f[i]\) 表示“ \(S\) 中以 \(i\) 结尾的子串”与“\(P\) 的前缀”能够匹配的最大长度,即:\[f[i] = \max\lbrace j \rbrace, 其中 j \le i 并且 S[i-j+1 \sim i] = P[1 \sim j] \]
下面介绍 \(next\) 数组的求法:
1. 初始化 next[1] = j = 0,假设 next[1 ~ i-1] 已求出,下面求解 next[i]
2. 不断尝试扩展匹配长度 j,如果扩展失败(下一个字符不相等),令 j 变为 next[j],直至 j 为0(应该重新从头开始匹配)
3. 如果能够扩展成功,匹配长度 j 就增加 1。next[i] 的值就是 j。
求解 \(f\) 与求解 \(next\) 的过程基本一致,具体参考模板代码。
#include <iostream>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int n, m;
char p[N]; // 模版串
char s[M]; // 模式串
int ne[N]; // next数组
int main()
{
// 注意,此处从下标1开始读入字符串,方便后续操作
cin >> n >> p + 1 >> m >> s + 1;
// 1. 预处理next数组,默认next[1] = 0,从2开始处理
for (int i = 2, j = 0; i <= n; i ++ ) {
// 当j不是第一个匹配点且匹配不成功时,从下一候选项尝试
while (j && p[i] != p[j + 1]) j = ne[j];
// 匹配成功
if (p[i] == p[j + 1]) j ++;
ne[i] = j;
}
// 2. 匹配
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 ++;
// 此时就是 P 在 S 中的某一次出现
if (j == n) {
// 输出匹配成功的起始位置
cout << i - n << ' ';
// 从下一候选项继续尝试匹配
j = ne[j];
}
}
return 0;
}
在上面代码的while
循环中,j
的值不断减小,j=ne[j]
的执行次数不会超过每层for
循环开始时j
的值与while
循环结束时j
的值之差。而在每层for
循环中,j
的值至多增加 \(1\)。因为j
始终非负,所以在整个计算过程中,j
减小的幅度总和不会超过j
增加的幅度总和。故j
的总变化次数至多为 \(2(N+M)\)。整个算法的时间复杂度为 \(O(N+M)\)。这样只需遍历一次 \(S\) 就可以得到 \(P\) 的所有匹配位置。
- 计算Partial_Table(或者说是计算模式串的最长公共前缀后缀长度列表)时的比较次数介于[m,2m],假设m是模式串的长度.
- 比较模式串和子串时比较次数介于[n,2n],最坏情形形如T="aaaabaaaab",P="aaaaa".