medal-dreams

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

请注意:这是我很久之前在 luogu 发的一篇题解(虽然没过),当时水平欠佳,对于 KMP 的理解也没现在深刻,看看就行。。。luogu题解网址

好处

对于我们通常的判断一个字符串是否是另一字符串的子串,通常会用直接判断的方式:即依次判断每个字符是否相同,当不同时就将其后移继续判断。若用 \(m,n\) 表示两个字符串的长度,这个的时间复杂度约为 \(O(mn)\)

但这样每个字符所做的贡献 (如果你不知道贡献是什么,请往下看) 就会被忽略,如果我们将每个字符的贡献使用,就可以实现 KMP 算法,时间复杂度为 \(O(n + m)\)

定义 (算法原理)

先看看 《算法竞赛进阶指南》 的定义:
KMP 算法,又称模式匹配算法,能够在线性时间内判断字符串 \(A[1 \sim N]\) 是否为字符串 \(B[1 \sim M]\) 的子串,并求出字符串 \(A\) 在字符串 \(B\) 中各次出现的位置。

所以 KMP 算法并不只是判断字符串是否是另一字符串子串的算法,也可应用在判断字符串子串个数、位置,与字符串重合等方面。

原理

KMP 的原理较难懂,这里先举出例子:
假设字符串 abababcd 中,按照朴素判断方法:

  1. 我们会先让 abab 中的 aabcd 判断。
  2. 发现相同后,我们会让 abab 中的 babcd 判断。
  3. 发现也相同,我们会让 abad 中的 a 再与 abcd 中的 c 判断,发现不相同。
  4. 于是程序就会让 abab 中的 a 重新与 abcd 中的 b 判断。
  5. ......

发现了什么?是不是判断 b 这一步似乎没有必要,因为 ab 已经确定与 ab 相同了,那么 a 一定和 b 不同,这就是这个字符的贡献。

那么 KMP 的想法就是记录这些字符的贡献,从而跳过这些无用的判断,从而加快算法,减少复杂度。
对于如何记录贡献呢,那么我们就得知道字符串的重复情况,让跳过时直接跳到上一个可以符合这一情况的时候,具体实现重复呢,例如字符串 abababaac

我们需要一个 next 数组的帮助,他的功能是记录上一个可以的字符。(这里不在介绍求 next 数组的朴素算法,和朴素求子串代码类似)

首先,对于最开始的 abababaac 我们

next[1] = 0;

具体原因是因为 字符 a 之前没有字符。

之后,我们依次判断 abababaac
abababaac 的重合情况,具体如下:

  1. 从第二位判断重合(因为如果是第一位就一定会重合,没有必要)
  2. 发现 ab 不相同,尝试跳到之前可能重合的地方。
  3. 发现没有之前的地方,所以向下判断,并记录
  next[2] = 0
  1. 发现 abababaacabababaac (从第三位开始)重合 aba ,记录
   next[3] = 1,next[4] = 2,next[5] = 3;

可以发现, next 每次记录的都是在前方的最近的相同字符点。
5. 发现再次不重合,通过 next 数组记录,跳到最前面的 a 再次判断。
......

通过这些,我们求出来 next 数组并且又通过 next 数组巧妙的节省了时间复杂度。
需要注意的是,在我们看来是移动,实际上在程序中只不过是换了个字符

图片解释

对应代码

ne[1] = 0;//最开头没东西 
	for(int i = 2,j = 0;i <= n;i ++)//j意味着现在跳到的字符的位置 
	{
		while(j > 0 && b[i] != b[j + 1]) j = ne[j];//跳位置,知道跳到最头或找到和这个点相同的 
		if(b[i] == b[j + 1]) j ++;//若相同,就向下判断 
		ne[i] = j;//标记此时位置的的next 
	}

需要注意由于 next 在 C++ 中属于关键字,所以我们只能使用 ne 或 nxt 这种类似的代替。

判断完自身后,我们就要判断此字符串和另一字符串,其过程与判断自身相似,不过有几点要注意:

  • 在判断另一字符串时我们没有字符串自身的长度,所以在找前的时候一定要算上自身的长度。
  • 在判断时我们会用另一数组(设这个数组为 p )这个数组记录的是在另一字符串中,我们这一字符串的最大长度。
    相关模拟与上类似,只不过判断的字符串切换成另一字符串,但使用的还是 next 数组。

代码

for(int i = 1,j = 0;i <= m;i ++)
	{
		while(j > 0 && (j == n || a[i] != b[j + 1])) j = ne[j];//这里与前面相同,只是增加j == n 来判断是否到达末尾 
		if(a[i] == b[j + 1]) j ++;//向下判断 
		p[i] = j;//用p记录最大的长度 
	}

回归本题

可以发现,要求输出 \(s2\)\(s1\) 的位置,也就是说明我们需要判断当 p[i] == n (其中 \(n\) 为要判断的字符串的长度)这个字符串所在位置即为 \((i - n + 1)\) (解释一下, \(i\) 为现在的末尾位置, \(i - n + 1\) 为现在的末尾长度减去字符串的长度,因为字符串长度会吞掉起始点,所以加一)。
之后输出 \(s2\) 的 next 数组。注意是 next 数组,而不是 p 数组,我才不会告诉你我为什么提醒这个

最后这个题就解决了。

std

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 +66;//注意 abs(s1),abs(s2) <= 1e6 
int ne[N],p[N];//next 数组 和 p 数组 
char a[N],b[N];//两个字符串 
int main()
{
	cin >> a + 1 >> b + 1;//输入,从下标一开始,a 表示 s1,b 表示 s2 
	int m = strlen(a + 1),n = strlen(b + 1);//m 为 a 的长度,n 为 b的长度 
	ne[1] = 0;//最开头没东西 
	for(int i = 2,j = 0;i <= n;i ++)//j意味着现在跳到的字符的位置 (可理解为将字符串移动的距离)
	{
		while(j > 0 && b[i] != b[j + 1]) j = ne[j];//跳位置,知道跳到最头或找到和这个点相同的 
		if(b[i] == b[j + 1]) j ++;//若相同,就向下判断 
		ne[i] = j;//标记此时位置的的next 
	}
	for(int i = 1,j = 0;i <= m;i ++)
	{
		while(j > 0 && (j == n || a[i] != b[j + 1])) j = ne[j];//这里与前面相同,只是增加j == n 来判断是否到达末尾 
		if(a[i] == b[j + 1]) j ++;//向下判断 
		p[i] = j;//用p记录最大的长度 
		if(p[i] == n)//当子串长度为 
			cout << (i - n + 1) << endl;
	}
	for(int i = 1;i <= n;i ++)
		cout << ne[i] << " ";
	return 0;
 } 

尾声

KMP 算法作为字符串匹配算法,在 CCF 举办的考试中似乎并不是很常见,大家在选择算法学习的话可以先寻找主流算法,在学习这些算法,当然考到了请别说我

posted on 2025-09-19 10:35  medal_dreams  阅读(5)  评论(0)    收藏  举报