KMP算法

引入

  • 给定两个字符串,设为text(文本串)和pattern(模式串),问模式串是否是文本串的子串,或者说在文本串中出现几次?

这个问题直观的想法是暴力,举一个例子:
text=“ABCABCABC”;
pattern=“ABC”;
暴力法如何求解呢?显然需要两个指针,一个循环text另一个循环pattern,假设用i循环text,用j循环pattern。设tot表示程序检查字符的总数。
第一次:ABCABC
              ABC

  • 检查三次字符,找到一个相同子串,tot+=3,i后退到text的第二个字符,j回到pattern 的初始位置。

第二次:ABCABC
                 ABC

  • 检查一次字符,tot++,不是子串,i移动到第三个位置,j回到pattern起始。 第三次:ABCABC

                   ABC

  • 检查一次字符,tot++,不是子串,指针移动。

第四次:ABCABC
                      ABC

  • 检查三次字符,tot+=3,找到一个子串,此时到text末尾,程序结束。
  • 在这个问题上,如果设两个字符串长度分别为m和n,那么由于两个字符串都要查到,所以时间复杂度至少也要大概O(m+n)
  • 在这个例子中text的长度是9,pattern的长度是3,总共查找字符的次数tot=8次。似乎接近这个复杂度。但是如果我们改一下两个字符串,使它们变成AAAAAAAB和AAAB这种,每一次都需要不停的遍历模式串,直到发现最后一个字符不符,直到文本串的最后,发现一个子串,那么时间复杂度就会退化成O(mn),而这种问题往往字符串长度都会达到105以上,所以这种做法不可取。
#include <bits/stdc++.h>//暴力
using namespace std;
const int MAXN=1e6+100;
char text[MAXN],pattern[MAXN];
int main(){
	scanf("%s%s",text,pattern);
	int text_len=strlen(text);
	int pattern_len=strlen(pattern);
	int ans=0;
	for(int i=0;i<=text_len-pattern_len;i++){
		int ret=i;
		for(int j=0;j<pattern_len;j++){
			if(pattern[j]==text[ret]){
				ret++;
			}
			else break;
			if(j==pattern_len-1) ans++;
		}
	}
	cout<<ans;
	return 0;
}

KMP

  • 可以看到,暴力做法时间复杂度退化的原因在于让文本串的指针后退的不合理,因为我们已经知道前面都一样了,没必要再让i回到文本串下一个位置,所以我们需要告诉程序应该在哪里进行下一次字符串的比较,这样,我们就需要对格式串进行预处理,得到KMP的核心-Next数组,应用这个数组,我们可以做到跳过一些字符的比较,避免文本串指针的回溯。
  • 首先我们应该理解几个定义(通俗一些):
    前缀:从字符串的第一个字符开始的某些连续字符。比如abcd,它的前缀是a,ab,abc,abcd这样四个。
    后缀:从字符串的某个字符开始直到字符串末尾的字符。比如abcd,它的后缀是d,cd,bcd,abcd这样四个。
  • 有了这些定义,我们就可以思考如何才能只回溯j,不回溯i。举一个极端的例子:text=AAAAAAAAB;pattern=AAAAB,在第一轮比较之后变成了这样的情况
    AAAAAAAAB                   (text)
    AAAAB                            (pattern)
  • 在第五个位置失配了,如果我们用肉眼看的话一下子就可以看出肯定是让pattern的第一个A到这个失配的位置继续比较,所以如果计算机能够这样做肯定是最优方案,这里就引入了一个Next数组。

Next数组

原理
  • 在KMP里面,Next数组其实是对pattern串进行的一个预处理,里面存的是所有前缀字符串的前缀和后缀相同的最大长度(或者说最长公共前后缀,不包括自身)。比如上面的AAAAB,对他进行预处理,我们首先把它的所有前缀写在下面:
    A
    AA
    AAA
    AAAA
    AAAAB
  • 第一个A,就一个字符,Next[1]=0;
  • 第二个AA,前缀和后缀相同且不包含自身的最大长度是1;
  • 第三个AAA,这个长度是2;
  • 第四个AAAA,这个长度是3;
  • 第五个AAAAB,因为前缀和后缀必然不相等(不包括自身),所以这个长度是0;
    这样,Next[1]=0,Next[2]=1,Next[3]=2,Next[4]=3,Next[5]=0。
  • 可能还是有点晕,这样,我们借助Next数组进行一轮的字符串匹配,还是这个例子。
    AAAAAAAAB                   (text)
    AAAAB                            (pattern)
  • 第五个位置失配了,我们看一下Next数组,Next[5]=0,那么j回溯到0也就是pattern串的第一个A,这就达成了我们刚开始优化的目的。

Next数组操作的基本原理明白了之后,我们首先就要试图写出Next数组的生成程序,可以思考一下,如何落实这个Next数组呢?

生成Next数组
  • 如果用暴力的方法,也就是每次去检查pattern的各个子串然后判断是否相等,显然不可行,那样KMP就没有什么意义了,所以我们应该从Next数组的含义出发而不是从字符串本身出发,Next数组中存放的是字符串每一个前缀子串的前缀和后缀相同的最大长度。那我们是否可以通过某些类似于递推的方式线性的推出Next数组的全貌呢?
    可以这样想:假设还是这个pattern串,AAAAB,为了方便,我们的Next从1开始,Next[0]置为0或者-1(方便一些),Next[1]=0是显然的,Next[2]也就是在Next[1]的基础上加一个字符,如果这个字符和上一个最长前缀的对应位置相等,那么Next应该相对于上一个+1,否则置0;再举一个例子:ABCABC,Next[1]=0,Next[2]=0,Next[3]=0,Next[4]=1(这里考虑的是ABC和A,借助Next数组回到ABC的A位置,一会代码实现的时候具体讲),Next[5]=2(ABC和AB),Next[6]=3(ABC和ABC),很巧妙。
  • 这样我们编写一个prefix_table(前缀表)函数。
void prefix_table(char* pattern,int len){//len表示模式串长度
    Next[0]=Next[1]=0;
    int j;
    for(int i=1;i<len;i++){
        j=Next[i];
        while(j&&pattern[i]!=pattern[j]) j=Next[j];//这个位置在下面详细讲解
        if(pattern[j]==pattern[i]) Next[i+1]=j+1;
        else Next[i+1]=0;
    }
}
  • i的作用是遍历字符串,j的作用是作为一个索引,在失配的时候找到应该从哪个位置开始重新计数(j=Next[j]).注意我们的Next数组是从1开始的,表示字符串中第几个位置。为了加深理解,举一个例子ABABAAB:
    第一圈循环,i=1,j=Next[1]=0,A和B不等,Next[2]=0;
    第二圈循环,i=2,j=Next[2]=0,A=A,所以Next[3]=Next[2]+1;
    第三圈循环,i=3,j=Next[3]=1,推出Next[4]=Next[3]+1=2;
    第四圈循环,i=4,j=Next[4]=2,推出Next[5]=Next[4]+1=3;
    第五圈循环,i=5,j=Next[5]=3,现在情况有些特殊了,出现了失配,需要Next数组找到j回溯的位置,Next[3]=1,所以j=1,依然失配,Next[1]=0,A=A,符合,此时j为0,故Next[6]=j+1=1,接下来同理。
  • 如果想要满足递推+1的关系,每次增加一个新的字符应该和前缀一一对应,这个可以通过i和j的索引简单实现;关键在于出现失配的时候,应该回溯到哪个位置。注意这个位置不一定是起始(考虑这样一个字符串abacababd,Next数组从[1,len]的正确输出是0 0 1 0 1 2 3 2 0 ),这个位置是已经求得的部分Next数组告诉我们的,其实我认为就是找上一次出现这个数的地方,这样一次一次的向前找,直到出现一个Next数组为0或者找到这样一个符合条件的位置。如果Next数组为0,只需要比较它和首字符是否相等;另外一种情况需要看前面有多少个字符,也就是最大公共前后缀,这些信息都在Next数组中。实在巧妙。
  • 这样获取Next数组的函数可以简写成下面这样。
void prefix_table(char* pattern,int len){
    Next[0]=Next[1]=0;
    int j;
    for(int i=1;i<len;i++){
        j=Next[i];
        while(j&&pattern[i]!=pattern[j]) j=Next[j];
        Next[i+1]=(pattern[i]==pattern[j])?j+1:0;
    }
}

Next数组和KMP的联系

  • 知道了Next数组以后,就可以对暴力程序进行优化,使得j在失配的时候每次回溯到Next[j]的位置上。
  • 我们可以对最开始的暴力程序进行kmp优化。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+100;
char text[MAXN],pattern[MAXN];
int Next[MAXN];
void prefix_table(char* pattern,int n){
    Next[0]=Next[1]=0;
    for(int i=1;i<n;i++){
        int j=Next[i];
        while(j&&pattern[i]!=pattern[j]) j=Next[j];
        Next[i+1]=(pattern[i]==pattern[j])?j+1:0;
    }
}
int main(){
	scanf("%s%s",text,pattern);
	int text_len=strlen(text);
	int pattern_len=strlen(pattern);
	prefix_table(pattern,pattern_len);
	//cout<<text_len<<' '<<pattern_len<<endl;
	int ans=0;
	int i,j;
	i=j=0;
	while(i<text_len){
		while(j&&text[i]!=pattern[j]) j=Next[j];
		if(text[i]==pattern[j]) j++;
		i++;
		if(j==pattern_len) ans++;
	}
	cout<<ans;
	//for(int i=1;i<=pattern_len;i++) cout<<Next[i]<<' ';
	return 0;
}

KMP的应用

  • 看一道板子题,洛谷P3375,在文本串中找目标子串。思路是做出子串的前缀表,再进行一次KMP寻找所有的子串并输出起始位置。
  • 需要注意的是strlen函数是O(n)的,所以弄一个len存一下char数组长度。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+100;
int Next[MAXN];
char s[MAXN],ss[MAXN];
int n,m;
void prefix_table(char* pattern){
    Next[0]=Next[1]=0;
    for(int i=1;i<n;i++){
        int j=Next[i];
        while(j&&pattern[i]!=pattern[j]) j=Next[j];
        Next[i+1]=(pattern[i]==pattern[j])?j+1:0;
    }
}
void kmp(char* text,char *pattern){
    int i,j;
    i=j=0;
    while(i<m){
        while(j&&text[i]!=pattern[j]) j=Next[j];
        if(text[i]==pattern[j]) j++;
        i++;
        if(j==n){
            cout<<i-j+1<<endl;
        }
    }
}
int main(){
    scanf("%s%s",s,ss);
    n=strlen(ss);//j
    m=strlen(s);//i
    prefix_table(ss);
    //for(int i=1;i<=n;i++) cout<<Next[i]<<' ';
    kmp(s,ss);
    for(int i=1;i<=n;i++) printf("%d ",Next[i]);
    return 0;
}
  • *欢迎留言指点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值