KMP 超清晰的详解(附图)

Upd on 2023.5.14:重述本篇文章部分内容,使得文章更加易懂。

前言

在我们今天的学习之前,常见的字符串算法除了 STL 之外就是哈希。但哈希有一定的错误概率,如果想要 100 % 100\% 100% 正确,就要双哈希,非常麻烦。那么,有没有一些算法,保证正确性的同时还拥有优秀的复杂度呢

这就是今天的主角:KMP 算法。

本文默认字符串下标从 1 1 1 开始。

算法讲解

KMP 是由三位大佬: D . E . K n u t h \mathcal{D.E.Knuth} D.E.Knuth J . H . M o r r i s \mathcal{J.H.Morris} J.H.Morris V . R . P r a t t \mathcal{V.R.Pratt} V.R.Pratt,取他们名字首字母,就是 K , M , P K,M,P K,M,P

原理

KMP 常用于解决单串匹配问题。即给定两个字符串 s s s t t t,问 t t t s s s 中的出现情况(出现次数或出现位置等)。

我们将 s s s 称为文本串, t t t 称为模式串。

一般的解法是对于 s s s 的每个位置都放一个 t t t 上去一位一位匹配。这种算法效率底下的原因是因为某些位置可能判断过很多次。

而 KMP 的优越性在于,每次匹配失败后,不会重头再来,而是根据之前匹配的“经验”跳到下一个可能可以成功匹配的地方,从而减少冗余信息的计算。

我们以下面的例子介绍:

现在文本串的第 5 5 5 位失配(匹配失败)了。KMP 会将模式串对齐至文本串的第 3 3 3 位。继续匹配,变成这样:

为什么他会跳到第 3 3 3 位?

与很多人理解的不同,它跳到第 3 3 3 位的原因是因为模式串本身的字符结构,而不是文本串。因为现在是第 5 5 5 位失配,证明前 4 4 4 位都已匹配成功。

而模式串的一二位与三四位是相等的,文本串的三四位又与模式串三四位是匹配的,所以直接挪到文本串第三位,可以保证一二位相等。

换言之,如果我们能知道 t 1 ∼ i t_{1\sim i} t1i 的最后若干位与前面若干位相同,那么在 i i i 失配之后就可以直接跳过这么多位,从而大大增加效率。

用公式表示,我们对每一个 i i i,求一个 k m p i kmp_i kmpi,使得 t 1 ∼ k m p i t_{1\sim kmp_i} t1kmpi t i − k m p i + 1 ∼ i t_{i-kmp_i+1\sim i} tikmpi+1i 这两段是相同的,即前后缀相同的长度。

但是这个方法不够完美。比如在 mmmmmmmmmmmmmmmmh 中查找 mmmmmms,你会发现每一次都是到最后一位才失配,又要跳回第一位重新匹配,比较恶心人。

所以我们要求的是最长长度而不是任意长度。

那么,问题是,如何求 k m p kmp kmp 数组?

考虑使用一种类似递推的方式。对于一个位置 i i i,如果 t i = t k m p i − 1 + 1 t_{i}=t_{kmp_{i-1}+1} ti=tkmpi1+1,那么我们发现一定有 k m p i = k m p i − 1 + 1 kmp_i=kmp_{i-1}+1 kmpi=kmpi1+1,如图:

其实就是在 k m p i − 1 kmp_{i-1} kmpi1 的基础上加上了上图中两个蓝色的部分。

那如果不相同呢?我们还要往前跳,那又要跳到哪里呢?

不能随便跳,因为 t i = t k m p i − 1 + 1 t_{i}=t_{kmp_{i-1}+1} ti=tkmpi1+1 的前提是 t i − 1 = t k m p i − 1 t_{i-1}=t_{kmp_{i-1}} ti1=tkmpi1。所以我们考虑再次将这个递推式进化,变成: t i − 1 = t k m p i − 1 = t k m p k m p i − 1 t_{i-1}=t_{kmp_{i-1}}=t_{kmp_{kmp_{i-1}}} ti1=tkmpi1=tkmpkmpi1。那我们就可以重复上述的过程,不停的判断是否相等,如果不相等就继续嵌套。

如图,匹配失败后,红色框往它的 k m p kmp kmp 值走了。走完发现这回相等了,于是 k m p i = k m p k m p i − 1 + 1 kmp_i=kmp_{kmp_{i-1}}+1 kmpi=kmpkmpi1+1,即蓝色部分。

这样的时间复杂度是对的,至于怎么分析,要用到均摊,作者比较菜,故省略。

那么边界是多少呢?显然 k m p 0 = 0 kmp_0=0 kmp0=0。问题是 k m p 1 kmp_1 kmp1 是多少?按定义来说,前后缀长度相同的长度应该是 1 1 1 呀!

但是这样就有问题了。比如有一个 i i i,它的 k m p kmp kmp 跳着跳着来到了 1 1 1。此时应该比较的是 t i t_i ti t k m p 1 + 1 = t 2 t_{kmp_1+1}=t_{2} tkmp1+1=t2。如果不相等,又跳回到 k m p k m p 1 = k m p 1 = 1 kmp_{kmp_1}=kmp_{1}=1 kmpkmp1=kmp1=1,你就会发现它出不来了。

有人说,加个特判不行吗?不好意思,不行。你并不知道你是不是第一次进 k m p 1 kmp_1 kmp1。如果你一到 1 1 1break 那将无法匹配第一位,从而导致 k m p i kmp_i kmpi 全部变成 0 0 0

所以,为了方便,我们将 k m p 1 = 0 kmp_1=0 kmp1=0,这样一到 0 0 0 我们就停手,可以保证能够正确地求出 k m p kmp kmp 数组。

void init(){
	kmp[0] = kmp[1] = 0;
	for(int i=2;i<=m;i++){
		int j = kmp[i - 1];
		while(j > 0 && t[i] != t[j + 1])
			j = kmp[j];
		if(t[i] == t[j + 1])
			kmp[i] = j + 1;
	}
}

那么,现在就是用 t t t 匹配 s s s 了。其实这个过程也是相当相似的,只是我们把 t t t t t t 自己匹配换成了 t t t s s s 匹配,所以代码也很容易:

	int now = 0;//当前匹配到什么位置
	for(int i=1;i<=n;i++){
		while(now > 0 && s[i] != t[now + 1])
			now = kmp[now];
		if(s[i] == t[now + 1])
			++now;
		if(now == m)//匹配成功了
			printf("%d\n", i - m + 1);
	}

组合起来再输出 k m p kmp kmp 数组就可以过掉 P3375 啦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值