KMP算法及相关例题整理(未完待续)

本文详细介绍了KMP算法,这是一种用于高效搜索字符串中子串出现次数的算法。通过分析失配情况,避免了不必要的字符比较,从而达到线性时间复杂度。文章提供了多个例题和代码,帮助理解KMP算法的应用,并给出了模板代码。
摘要由CSDN通过智能技术生成

目录:
一、KMP算法
二、例题及代码编写指导
三、模板

一、KMP算法

在计算机科学中,KMP 算法(Knuth–Morris–Pratt string-searching algorithm)是用于搜索在一个字符串中某个字串的出现次数的算法,该算法考察:在失配发生时,应该在何处开始匹配下一个字符,以免于重新比较先前已经匹配的字符。
这个算法在 1970 年被 Donald Knuth 和 Vaughan Pratt 提出构想,然后被 H.Morris 独立发明出来。这是首个具有线性时间的字符串匹配算法。三人联合在 1977 年发布了此算法。在 1969 年,Matiyasevich 在研究字符串形式匹配认知问题时,发现了一个相近的算法,该算法用一个二维图灵机写出。
(节选自维基百科)

来看下面的问题:
给一个主字符串(长度约 100 万)和一个子字符串(长度约 1000),问:子串(也称为模式串)在主串中一共出现多少次?(存在如下的重叠情况,例如主串为 ABABABA ,子串为 ABA ,则子串在主串中出现 3 次)
最直接的想法是将子串在主串的首个(第 0 个)字符开始,遍历子串的每个字符,然后将子串的首个字符对准主串的第 1、2、3、……个字符继续开始新一轮匹配。
显然,只要字串比较长,这样的比较的时间成本将是巨大的。如果设主串和子串的长度为 n 、m ,那么解决这个问题的时间复杂度达到了约O(mn) 。
是否存在一些特殊情况,使得比较次数可以大大减少呢?我们以如下的字符串匹配为例。

(第一部分的大多数内容是根据 https://www.cnblogs.com/yjiyjige/p/3263858.htmlhttps://blog.csdn.net/x__1998/article/details/79951598 整理的)

对于这样的情况,显然可以看出,不需要将子串的第 0 个字符 A 与主串的第 1 个字符 B 对齐并开始新一轮的比较,而可以将子串向右移动更多位置。

将子串的开头与主串的第 3 个字符 A (注意:字符串的首个字符记为第 0 个)对齐,然后直接从第 6 个字符 D 继续比较即可。
设在某次比较中,主串的第 i 个字符与子串的第 j 个字符不匹配。为了不搅乱思维,我们尝试让 i 不要变动,而只变动 j 。也就是说,i 仍然增加,将 j 变为某个值 k ,然后继续比较。另外,我们也不会将整个数组存储的值(即子串)都进行偏移,因为这样会花费大量的时间。所以如果确能找到一个只改变 j 的方法,自然是最好的。

为了方便找出规律,我们再来看几个例子:
在这两例中,当 i = j = 3 和 i = j = 5 时两个字符串分别失配。






这几种情况中,我们可以看出在 j 之前的字符串中,前几个字符和后几个字符都对应相等(都是从左往右看)。于是,我们将子串向右移动若干位,使得子串再一次与主串中重复出现的那部分对上。
移动超过 1 个单位以后,就节省了大量的算力。当然,不是所有情况都可以一下就移动这么多步。比如:

在主串的每一个位置,都有可能发生不匹配。所以我们要对每一个位置都分别求解 ,并将结果保存在 next 数组中:next[j] 代表子串在第 j 位失配以后光标 j 需要移动到的位置 。
那么怎样快速求出整个数组的值呢?
我们知道,能够一次将子串(也成为模式串)移动大于 1 个单位的条件是子串的前若干位与后若干位在从左到右的顺序下都相等。下面以主串 abababca 为例来讲解:

当不匹配发生在 j = 2 时(这里的 j 与图上的 j 意义不同),因为该位置之前模式串的前缀和后缀(这里说的前缀和后缀均不包括模式串自身)都不相同,所以并没有办法多移动 1 位,只能老老实实将 j 重新归零。


当模式串与主串在第 3 位失配,模式串在该位置之前的部分的前后缀有 1 位相同。复制一个模式串,将新复制出来的串的前缀与原模式串的相同后缀对齐,显然接下来继续与主串匹配时要从模式串的第 1 个字符开始,所以 next[3] = 1 。
同理 next[4] = 2 。


当模式串与主串在第 6 位失配,模式串在该位置之前的部分的前后缀有 4 位相同。复制一个模式串,将新复制出来的串的前缀与原模式串的相同后缀对齐,显然接下来继续与主串匹配时要从模式串的第 4 个字符开始,所以 next[6] = 4 。
当模式串与主串在第 7 位失配,模式串在该位置之前的部分没有相同的前后缀,所以 next[7] = 0 。这时候同样只能从模式串的第 0 位开始一字不漏地继续匹配主串。

根据如上举例,尝试总结并推广规律:
当模式串与主串不匹配,设此时模式串核对到第 j 个字符(j > 0)。则主串的下一个字符与模式串匹配时,需要与模式串的第 next[j] 个字符匹配。而 next[j](即前文提到的 k)的值就是模式串中前后缀相同的长度。

这里有一段能够比较快地根据上述规律求出 next 数组的代码,但是经过我的一系列艰苦卓绝的搜索与尝试,一直无法给出严格的推导过程。但是代入若干样例手算,发现这段代码确实能够给出正确的结果。
为了能够尽快投入应用,这里暂时不予对算法进行推导,而是先看例题,在博文的最后我会给出模板。
上述操作只更新了除 next[0] 以外的项,而 next[0] 也可以记为 -1 以外的值。

二、例题及代码编写指导
这一部分的例题目前均来自 https://blog.csdn.net/qq_38891827/article/details/80501506 。在这里对题目及其 AC 代码作一些补充说明。
1、HDU 1711

注意这题只需要在第一次找到数列 {bm} 后就给出该数列首项的下标(从 1 开始),所以在第一次找到的时候直接传回结果即可。找不到则输出 -1 。

AC 代码:(842 ms)

inline int KMP_Match_once(const int* text, const int* pattern, int* Next) {
   
	int i = 0, j = -1;
	while (i < m) {
   
		if (j == -1 || pattern[i] == pattern[j]) {
    ++i, ++j, Next[i] = j; }
		else {
    j = Next[j]; }
	}//Construct the Next array.
	i = 0, j = 0;
	while (i < n) {
   
		if (j == -1 || text[i] == pattern[j])++i, ++j;
		else j = Next[j];
		if (j == m)return i - m + 1;
	}
	return -1;
}
int main() {
   
	scanf("%u", &t); ++t; Next[0] = -1;
	while (--t) {
   
		scanf("%d%d", &n, &m);
		for (int i = 0; i < n; ++i)scanf("%d", &a[i]);
		for (int i = 0; i < m; ++i)scanf("%d", &b[i]);
		printf("%d\n", KMP_Match_once(a, b, Next));
	}
	return 0;
}

二、HDU 1686

也许你还没来得及好好理解 KMP 算法的代码,但是我们也完全可以仿照上题的代码来完成本题。与上题不同的是,这题要求统计单词 W 的出现次数,所以每当模式串 W 走完时,都需要将统计次数用的变量增加 1 。
虽然上面贴出的原博客的地址中也给出了模板,但是每个 ICPC 竞赛选手都应该整理出自己的模板。同样的,我给出的模板也不一定适合每一个代码手。
这里采用 unsigned(unsigned int)作为主要的数据类型,因此记 next[0] = 4294967295 (0xFFFFFFFF) 。

AC 代码(78 ms):

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值