一.KMP算法简介
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,称之为 Knuth-Morria-Pratt 算法,简称 KMP 算法。该算法相对于 Brute-Force(暴力)算法有比较大的改进,主要是消除了主串指针的回溯,从而使算法效率有了某种程度的提高。
KMP算法的核心思想:利用已匹配的信息来减少回溯的次数
KMP算法实际上解决的是一个字符串匹配的问题,即从一个目标字符串(通常非常长)中找到与给定字符串(也称为模式串)相匹配的字串的位置。
在匹配过程中我们可以尝试让我们的主串中的指针不再回退,同时我们子串的指针在匹配的时候也回退到指定的位置,而不是开头的位置。
二.next数组
KMP 的精髓就是 next 数组:也就是用 next[j] = k;来表示,不同的 j 来对应一个 K 值, 这个 K 就是你将来要移动的 j要移动的位置。而 K 的值是这样求的 。
next数组的使用
现在来讨论一下,next数组使用的细节。先前只说通过next数组来重新对齐,但其实对齐后,仍然会从主串的“失配”位进行比较,只是子串比较的位置变了。子串比较的位置应该是前缀的后一个字符,而由于next指出的是前缀的长度,那么next的值其实就是前缀后一个字符的位置。综上,“失配”后,主串的比较位置不变,子串的比较位置就是对应的next值(这部分会体现到之后的代码当中)。
那么next数组怎么计算呢,下面我们将给个代码解释
nex[0] = 1; //初始化默认0的位置指向字符串的第一个
for (int i = 2, j = 0; i <= lenb; i++) { //这里从i指针从模式串的第二个开始
while (j && b[i] != b[j + 1]) j = next[j]; //如果不匹配,退回
if (b[i] == b[j + 1]) //如果匹配,记录当前nex,并使j++,准备下一次比较
j++;
next[i] = j;
}
我们可以参照一下例子,理解此代码
三.匹配过程
注意匹配过程是在主串和模版串之间进行的,与next数组过程不同(next数组计算仅在模版串中进行)。
for (int i = 1,j=0; i <= lena; i++) { //这里i指针指向的就是主串的第一个位置了
while (j && a[i] != b[j + 1]) j = next[j]; //下面操作和处理next数组差不多
if (a[i] == b[j + 1])
j++;
}
同样我们也可以通过例子来理解匹配过程。
四.例题分析
P3375 【模板】KMP
题目描述
给出两个字符串 s1 和 s2,若 s1 的区间 [l,r] 子串与 s2 完全相同,则称 s2 在 s1 中出现了,其出现位置为 l。
现在请你求出 s2 在 s1 中所有出现的位置。
定义一个字符串 s 的 border 为 s 的一个非 s 本身的子串 t,满足 t 既是 s 的前缀,又是 s 的后缀。
对于 s2,你还需要求出对于其每个前缀 ′s′ 的最长 border ′t′ 的长度。
输入格式
第一行为一个字符串,即为 s1。
第二行为一个字符串,即为 s2。
输出格式
首先输出若干行,每行一个整数,按从小到大的顺序输出 s2 在 s1 中出现的位置。
最后一行输出 ∣s2∣ 个整数,第 i 个整数表示 s2 的长度为 i 的前缀的最长 border 长度。
输入输出样例
输入 #1复制
ABABABC ABA
输出 #1复制
1 3 0 0 1
数据规模与约定
本题采用多测试点捆绑测试,共有 3 个子任务。
- Subtask 1(30 points):∣s1∣≤15,∣s2∣≤5。
- Subtask 2(40 points):∣s1∣≤104,∣s2∣≤102。
- Subtask 3(30 points):无特殊约定。
对于全部的测试点,保证 1≤∣s1∣,∣s2∣≤106,s1,s2 中均只含大写英文字母。
KMP算法的模版题,通过分析代码,帮助我们更好的理解KMP算法
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int j, lena,lenb;
int nex[N];
char a[N];
char b[N];
int main()
{
scanf("%s%s", a + 1, b + 1); //让字符串从数组1位置开始
lena = strlen(a+1);
lenb = strlen(b+1); //主串和模版串的长度
nex[0] = 1;
for (int i = 2, j = 0; i <= lenb; i++) { //next数组的计算
while (j && b[i] != b[j + 1]) j = nex[j];
if (b[i] == b[j + 1])
j++;
nex[i] = j;
}
for (int i = 1, j = 0; i <= lena; i++) { //主串和模版串匹配
while (j && a[i] != b[j + 1]) j = nex[j];
if (a[i] == b[j + 1])
j++;
if (j == lenb) //即j等于模版串长度时,从主串中找到模版串
{
cout << i - lenb + 1 << endl; //输出其位置
j = nex[j]; //回退,便于继续下一次匹配
}
}
for (int i = 1; i <= lenb; i++) {
cout << nex[i] << " ";
}
return 0;
}
P2957 [USACO09OCT] Barn Echoes G
这道题即可以用KMP算法做也可以用我们之前介绍的字符串哈希做,这里我们就介绍KMP算法的实现过程
第一个串的最后的部份 yzooo
跟第二个串的第一部份重复。第二个串的最后的部份 mo
跟第一个串的第一部份重复。所以 yzooo
跟 mo
都是这 22 个串的重复部份。其中,yzooo
比较长,所以最长的重复部份的长度就是 55。
根据这句话我们可以知道,其实我们只需要互为模板串,通过两次KMP求其各自的最大前后缀,最后比较取最大值即可
#include<bits/stdc++.h>
using namespace std;
#define N 1000005
int j, lena, lenb, ansa, ansb, m;
int nex[N];
char a[N];
char b[N];
int main()
{
scanf("%s%s", a + 1, b + 1);
lena = strlen(a + 1);
lenb = strlen(b + 1);
nex[0] = 1;
for (int i = 2, j = 0; i <= lenb; i++) { //将b串当做模版串,计算next数组
while (j && b[i] != b[j + 1]) j = nex[j];
if (b[i] == b[j + 1])
j++;
nex[i] = j;
}
j = 0;
for (int i = 1; i <= lena; i++) { //a串为主串,b串为模版串,进行匹配
while (j && a[i] != b[j + 1]) j = nex[j];
if (a[i] == b[j + 1])
j++;
}
ansa = j;
memset(nex, 0, sizeof(nex)); //清空next数组,进行第二次KMP
nex[0] = 1;
for (int i = 2, j = 0; i <= lena; i++) { //将a串当做模版串,计算next数组
while (j && a[i] != a[j + 1]) j = nex[j];
if (a[i] == a[j + 1])
j++;
nex[i] = j;
}
j = 0;
for (int i = 1; i <= lenb; i++) { //b串为主串,a串为模版串,进行匹配
while (j && b[i] != a[j + 1]) j = nex[j];
if (b[i] == a[j + 1])
j++;
}
ansb = j;
m = max(ansa, ansb); //最后比较,取最大值
cout << m << endl;
return 0;
}
通过以上介绍,是否对KMP有了一个更深的了解,没有明白也没关系,这里也给你们推荐了KMP算法的详细讲解视频
最后在送给大家一段话,与各位共勉,希望在我们迷茫时候能够得到宽慰。
一个人能走的多远不在于他在顺境时能走的多快,而在于他在逆境时多久能找到曾经的自己。 ————KMP