详谈KMP

详谈\(KMP\)

(看毛片)

$ KMP $ 可以说是一个十分古老的算法了,但是我之前一直一知半解(最多是知道好用,会用,但是没有弄明白复杂度方面的问题),今天终于弄明白了,快来记一下 $ awa $ 。

$ KMP $ 的名字由来是因为它是由 $ Knuth $ ,$ Morris $ 和 $ Pratt $ 三位大神共同发明的,所以取了三人的名字的首字母,叫 $ KMP $

1. $ KMP $ 原理:

那么首先, $ KMP $ 是干什么的?

答: $ KMP $ 是用来快速完成模式串匹配问题的。

典型问题就是给你一个字符串 $ A $ 和字符串 $ B $ ,问 $ A $ 中有多少子串和 $ B $ 相同,以及出现的位置。

对于这个问题,暴力的方法就是进行枚举, $ for $ 循环枚举文本串,然后一位一位的向后比较,一旦发现模式串匹配不上就从模式串的头开始枚举。

这样期望复杂度是常数大亿点 $ O(N+M) $ 的,但是还是很容易卡到 $ O(N \times M) $ 的 $ QwQ $ 。

那么, $ KMP $ 为什么最多是 $ O(N+M) $ 的呢?是因为 $ KMP $ 的精髓在于假如这一位失配了,就可以直接一次性跳到最长的,还能匹配上的位置,大大减少了复杂度。

$ KMP $ 的核心思路就是一旦失配,就跳到当前匹配到的模式串位置的真前缀的真前缀和真后缀最大相同的位置

显然不像人话qwq

那么,我们来给他翻译一下:

假设有这么一个文本串和模式串:

文本串:abcabcabce
模式串:abcabce

我们对他们进行匹配:

首先,为了方便讲述,我们假设:用 $ i $ 来表示当前文本串匹配到的位置,$ il $ 表示当前进行的匹配的开头,$ j $ 表示当前模式串匹配到的位置)。

可以发现当我们匹配到 $ i=j=7 $ 的时候文本串是 “$ c \(” 但是模式串是 “\) e $” ,失配了。

那么我们这个时候需要跳到 $ j=0 $ 和 $ i=il+1 $ 吗?

我们可以发现:我们可以直接变到 $ j=3 $ 和 $ i++ $ 。

为什么呢?

以为首先匹配到了第 $ 7 $ 位代表着什么呢?

答:代表着 $ 1 $ ~ $ 6 $ 位都已经匹配过并且匹配成功了。

那么对于 $ 1 $ ~ $ 6 $ 位来说,它有一个前缀: $ abc $ 和后缀的 $ abc $ 是相同的。

我现在能匹配到第 $ 7 $ 为位就说明我的后缀 $ abc $ 和文本串是一样的(可以匹配),所以也就是说直接将我的前缀 $ abc $ 移过来还是可以和文本串匹配上的。

所以就可以变成:

文本串:abcabcabce
模式串:   abcabce

就好了 $ awa $ 。

这就是那句:“跳到当前匹配到的模式串位置的真前缀的真前缀和真后缀最大相同的位置” 的意思。

模式串就是 $ 1 $ ~ $ 7 $ 位:$ abcabce $ 。

模式串的真前缀就是 $ 1 $ ~ $ 6 $ 位:$ abcabc $ 。

真前缀就是 $ 1 $ ~ $ 5 $ 位($ abcab \()的所有前缀: \) a,ab,abc,abca,abcab $(就是除了模式串的前缀本身的所有前缀)。

真后缀就是 $ 2 $ ~ $ 6 $ 位($ bcabc \()的所有后缀: \) c,bc,abc,cabc,cabcb $(同理)。

最大相同的位置求出来之后可以发现就是 $ 3 $ : $ abc $ 。

这就是 $ KMP $ 的移位法则。

2. $ KMP $ 代码实现:

首先,我们先想一点简单的东西:

如果 $ kmp[i] $ 代表以当前为结尾的前缀的真前、后缀的最大相同位置。

那么就比如上文的例子的话, $ kmp[6] $ 就等于 $ 3 $ $ awa $。

假如我们已经处理好了 $ kmp[i] $ 数组,那么进行模式串匹配时就可以这么做:

Code:

	//a[i]是文本串  la是文本串的长度 
	//b[i]是模式串  lb是模式串的长度 
    int j=0;//j指向就是模式串当前已经匹配到了第几位 
    for(int i=1;i<=la;i++)
	{
		while(j&&b[j+1]!=a[i])	j=kmp[j];//就是如果j已经匹配了一部分但是
										 //当前的下一位匹配不上,已经无法拓展了, 
										 //就跳到上一个可以跳的位置 
		if(b[j+1]==a[i])	j++;//然后如果下一位是可以匹配的,那么就开始匹配模式串的下一位 
		if(j==lb)	//如果已经把模式串全都匹配成功了 
		{
			…………
			……
			………………
			//就进行操作(比如记录或者是直接输出awa) 
		}
	}

然后那让我们看一下 $ kmp[i] $ 数组怎么处理

可以发现 $ kmp[i] $ 其实对于模式串来说就是自己匹配自己,就是看当前枚举到的位置最多可以匹配上多长的真前缀。

(可以自己意会一下 $ awa $ )

所以:

Code:

	int j=0; 
    for(int i=2;i<=lb;i++)//一定记住,因为是真前缀(最短是1,且总长大于1),所以要从开头+1处开始枚举
						  //(我这里开头是1,所以这里是从i=2开始) 
	{
		while(j&&b[i]!=b[j+1])	j=kmp[j];
    	if(b[j+1]==b[i])	j++;
    	kmp[i]=j;//j就是最长能匹配到哪里 
    }

3. 关于时间复杂度:

曾经有一个蒟蒻,因为别的题解里都没有具体写这方面的内容,所以他一直对这方面似懂非懂,直到被机房里的某巨佬($ stO $ %%% $ whc $ %%% $ Orz $ )一语道破,他瞬间明了之后,就来写了这篇题解 $ QwQ $ 。

所以如果你已经懂了为什么时间复杂度是对的可以直接跳过\(qwq\)

我觉得如果对于时间复杂度有问题大概可能都是因为这句:

	for(int i=1;i<=la;i++)
	{
		while(j&&b[j+1]!=a[i])	j=kmp[j];
		………… 
		……
		……………… 
	}

(一层 $ for $ 循环加一层 $ while $ 循环不是一共两层循环吗 $ QwQ $ ?按理说最坏的情况下不会被卡到 $ O(n^2) $ 吗 $ QAQ $ ?)

如果你对这个问题有疑惑,说明你对 $ KMP $ 的巧妙程度还是没有足够的认识。

对于这个疑惑,一个问题就可以解决:

$ Q $ :把一个数 $ +1 $ 复杂度是多少?

$ A $ :肯定是 $ O(1) $ 啊!

$ Q $ :那把一个数 $ +k $ 呢,复杂度是多少?

$ A $ :肯定也是 $ O(1) $ 啊!(除非你用的是A+B Problem 题解区里的方法)

对,就是这个道理。

$ KMP $ 里的那个 $ while $ 循环看着挺唬人的,但是其实没有任何鸟用,因为你会发现,其实一共就有两种操作:

  1. $ i $ 和 $ j $ 同时 $ +1 $ 。

  2. $ i $ 还是 $ +1 $,但是 $ j $ 变小。

你会发现:$ 1 $ 操作就是我们的第一个问题。

它会进行 $ n \times O(1) = O(n) $ 次。

$ 2 $ 操作其实就是将 $ i $ 和 $ j $ 的距离 $ +k $ ,这就是我们的第二个问题。

那么,距离最多为 $ n $ ,就是最多 $ O(n) $ 次(如果每次都是 $ +1 $ 的话 $ awa $ )。

如果你还是不懂,那我们来换位思考一下:

假如你是一个出题人想要卡 $ KMP $ 那你要怎么卡?

肯定是让那个 $ while $ 循环循环的次数尽可能地多啊 $ awa $ 。

那怎么弄呢?

可以发现其实就是让模式串都是同一个字母,比如:$ aaaaa $ 。因为这样,$ kmp $ 数组就全都是 $ 1,2,3,4……$,这样每次匹配不上的时候,就相当于只是 $ j-- $

然后还要让它尽可能匹配不上,意思就是文本串是类似于:

$ aa…(lenb-1个a)… a $ $ b $ $ aa…(lenb-1个a)… a $ $ b $ $ ………………$

就比如这样:

文本串:aaaabaaaabaaaabaaaaa
模式串:aaaaa

然后,你就会发现:

除了第 $ lb $ 位以外,其他都不会有变动,然后第 $ lb $ 位只会变动 $ O(lb) $ 次。

这平均下来不就是每次动一下吗

测试一下( $ num $ 就是每次 $ while $ 循环的执行次数):

#include<bits/stdc++.h>
using namespace std;
int kmp[1000005];
int la,lb;
char a[1000005],b[1000005];
int main()
{
    cin>>a+1>>b+1;
    la=strlen(a+1),lb=strlen(b+1);
	int j=0;
    for(int i=2;i<=lb;i++)
	{
		while(j&&b[i]!=b[j+1])	j=kmp[j];
    	if(b[j+1]==b[i])	j++;
    	kmp[i]=j;
    }
    j=0;
    int num=0;
    for(int i=1;i<=la;i++)
	{
		num=0;
		while(j&&b[j+1]!=a[i])	num++,j=kmp[j];
		cout<<"==="<<num<<'\n';
		if(b[j+1]==a[i])	j++;
	}
    return 0;
}

$ input: $

aaaabaaaabaaaabaaaaa
aaaaa

$ output: $

0 0 0 0 4 0 0 0 0 4 0 0 0 0 4 0 0 0 0 0

\(awa\)

4. 例题:

那么,都学到这里了:

KMP模板

Code:

#include<bits/stdc++.h>
using namespace std;
int kmp[1000005];
int la,lb; 
char a[1000005],b[1000005];
int main()
{
    cin>>a+1>>b+1;
    la=strlen(a+1),lb=strlen(b+1);
	int j=0;
    for(int i=2;i<=lb;i++)
	{
		while(j&&b[i]!=b[j+1])	j=kmp[j];    
    	if(b[j+1]==b[i])	j++;    
    	kmp[i]=j;
    }
    j=0;
    for(int i=1;i<=la;i++)
	{
		while(j&&b[j+1]!=a[i])	j=kmp[j];
		if(b[j+1]==a[i])	j++;
		if(j==lb)
		{
			cout<<i-lb+1<<'\n';
			j=kmp[j];
		}
	}
	for(int i=1;i<=lb;i++)
		cout<<kmp[i]<<" ";
    return 0;
}

还有一道很不错的题目:

P4696 [CEOI2011] Matching

这是一道 $ KMP $ 好题,需要你对 $ KMP $ 有较深的理解 $ awa $ 。

continue===>

posted @ 2024-07-11 14:53  YT0104  阅读(2)  评论(0编辑  收藏  举报