目录
一年之前学的KMP,那时候对着课件上的PPT和网课视频一直在发呆,用手不停地在模拟代码的实现过程,反复地让自己“被动理解”这个算法,结果很明显,假装懂了终究还是欺骗自己。把课件上的代码原封不动地提交一下,这件事让我知道昨天还自信满满地以为我已经掌握了KMP算法。但是这次不一样,经过HDU和LeetCode的捶打之后,思路突然就打开了,写篇博客纪念一下。
题目分析:
实际上KMP算法就是去匹配一个文本串S中的某个给定模式串P(子串),暴力地去匹配,很简单,复杂度也就跟着上来了。KMP则是把暴力法中不必要的匹配过程压缩掉。
比如我们要匹配 " abce abce abcf " 中的 " abce abcf "(注意我的空格是为了方便观察特点):
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
a | b | c | e | a | b | c | e | a | b | c | f |
a | b | c | e | a | b | c | f |
两个串匹配到第八位,出现不同字符,暴力地去匹配,应当是模式串右移一为,在从头和新一位开始比较:
第一位就不匹配,再来:
反复移动一直到这样:
匹配成功了!我们一共移动了四次模式串。但是你会发现从第一步开始,后面的所有步都是傻子行为,因为在第一步中,我们已经知道文本串中的前7位和模式串已经匹配上了,再右移一位极有可能会打乱这已经匹配好的7位公有部分(例子有点特殊,可以简化思考过程),那么这部分的移动都是没有意义的了。仔细观察模式串 " abce abcf ",它包含了两个重复的部分 " abc ",我们可以用这个部分来简化移动。
我用i来代表当前文本串S的匹配位数,j来代表模式串P的匹配位数。暴力匹配下,每一次i和j都要回退到一个起点来从头匹配。我们希望减少回退的位数或者尽量不回退,这里我们选择不让i去回退,利用模式串中的公共部分只进行j的回退。
在第一步中,两个串中的5~7位都是 " abc ",而在模式串中,abc曾经在之前的某个位置出现过,那么下一次匹配的时候,我可以直接把曾经出现过的那个abc直接与文本串对齐,因为在这两个状态中间的所有状态都会破坏abc这个前缀的相对位置造成错位,不可能使两个串匹配。
那么体现在刚才的模拟过程,我们只需要移动两次就可以找到P在S中的位置了,只不过我们需要额外保存P中公共部分的下标的相对关系,以便于匹配不成功,我该把j回退到哪个位置,也就是当前位置字符,在P中的上一个前缀能回退到哪个位置。比方说在 " abce abcf "中,f的前缀abc(5~7)可以回退到(1~3)的位置,我们就可以把(1~3)的那个abc拿过来与文本串的位置匹配,也就是下面这个过程:
那么,我们接下来的任务就是统计模式串中的公共部分的下标(即找到j回退的位置),利用这个关系来进行KMP快速匹配。
解题过程:
我们用一个辅助数组f [ ]来记录j的回退位置,对于刚才的例子," abce abcf " 对应的f数组如下(为了拟合字符串的存储规则我令下标从0开始):
j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
P[j] | a | b | c | e | a | b | c | f |
f[j] | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 0 |
解释一下,假如到 j = 7 不匹配了,那么我们就找到j的前一位第6位。f [ 6 ] = 3,那么我们就让j回退到3去和S [ i ]匹配,看一下是不是和上面举得例子贴合上了?下面我给出求解f数组的代码:
int f[MAXN];
void fail(string P)
{
f[0] = 0; //初始化
for (int i = 1, j = 0; i < m; i++)
{
while (j > 0 && P[i] != P[j])
{
j = f[j - 1];
}
if (P[i] == P[j])
{
j++;
}
f[i] = j;
}
}
编码方式不唯一,递归也可以实现,小细节大家可以自行思考。
KMP匹配:
/**
返回P在S中第一次出现的下标
*/
int KMP(string S, string P)
{
int n = S.size();
int m = P.size();
for (int i = 0, j = 0; i < n; i++)
{
while (j > 0 && S[i] != P[j]) //本位不匹配,利用f去回退直到公共前缀的下一位匹配成功
{
j = f[j - 1];
}
if (S[i] == P[j])
{
j++;
}
if (j == m) //j匹配到P的最后一位了,说明匹配成功,返回位置
{
return i - m + 1;
}
}
}