KMP算法用于优化字符串匹配。
首先想到的应当是暴力匹配字符串,具体操作如下:
输入一个待匹配字符串S,一个用于匹配的模式串P,对于S字符串中的每一个字符,都用字符串P去匹配,若其中出现了不一致,则移动继续匹配从S的下一个字符开始匹配。简单来说就是用两个for循环去判断。
由于暴力算法去匹配字符串存在许多冗余操作,其时间复杂度达到了,因此由 D.E.Knuth、J,H,Morris 和 V.R.Pratt 共同研究出了KMP算法。KMP即分别取了三人的姓氏首字母。KMP算法的核心思想是用next数组跳过那些不可能的S[i],而直接从部分成功匹配的S[i]开始。其时间复杂度优化到了
。
概念解释:
字符串前缀:举例"ababc",当i = 4时(即考虑长度小于等于3的子串),其前缀分别为"a","ab","aba"。(假设下标从1开始)
字符串后缀:举例"ababc",当i = 3时(即考虑长度小于等于2的字串),其后缀分别为"c","bc"。(假设下标从1开始)
最大公共子串:举例"abab",最大公共字串即在字符串中满足:最大公共字串 = 某个前缀 = 某个后缀,且满足长度为最大。在举例中,"ab" = 前缀 = 后缀成立。因此最大公共字串长度 = 2。
下附完整代码(详解在注释中):
#include<iostream>
using namespace std;
const int N = 100010; // 题目要求 1e5 的数据大小
const int M = 1000010; // 题目要求 1e6 的数据大小
int n, m; //n, m 分别记录 p s 字符串的长度
char p[N], s[M]; //存储 p s 字符串
int ne[M]; //ne 数组与字符串 s 对应
int main()
{
//输入
cin.tie(0); //加快读入速度
cout.tie(0); //加快输出速度
cin >> n >> p + 1 >> m >> s + 1; // 从下标为 1 处开始输入
//构建 ne 数组 ne 数组只与模式串 p 有关 存储的是模式串 p 在长度为 i 的情况下前缀与后缀相同的最大长度(最大不超过 i - 1)
//ne[1] 默认值为1(由于长度为 1 时不存在长度小于 1 前后缀) 因此从 2 开始
for (int i = 2, j = 0; i <= m; i++)
{
//每一次循环保证 p[1, j] == p[i - j + 1, i] && p[i] == p[j + 1] 成立
while (j && p[i] != p[j + 1]) j = ne[j]; //不断寻找能够使得上面性质成立的 最大值
if (p[i] == p[j + 1]) j++; //由于又查找到p[i] == p[j + 1], 最大公共字串长度延长一位
ne[i] = j;
}
//KMP匹配过程 与构造 ne 数组过程类似
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++;
if (j == n)
{
cout << i - j << " "; //输出成功匹配的子串的第一个字符下标
j = ne[j];
}
}
}
值得一提的是,KMP算法主要由构造ne数组以及字符串匹配两个部分,这两个部分的代码几乎一样。这是因为对于ne数组的构造过程其实就是模式串P 与模式串P自身的匹配,使得对于每一个不同的(i, j)求得其最大公共字符串的长度 ne[j]。因此,如果弄懂了其中一个部分,另一个部分也就可以类似的进行理解了。