【字符串系列】字符串匹配(KMP)

KMP算法是一种字符串匹配算法,由三个老外发现的,因此算法就用三个老外名字首字母命名了。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。


KMP算法的关键是next数组的求解,并根据实际情况next数组可以进行各种活用。


next数组是什么?

例如有一个字符串n,和一个字符串m,需要在n中匹配m,那么就需要对m进行next数组求解,next数组就是字符串的自匹配,返回失配位之前的最长公共前后缀!这样就利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。


 KMP 算法之所以难懂,很大一部分原因是很多实现的方法在一些细节的差异,重要的是领悟思想,根据实际情况求next数组。我浏览过网上的一些博文,求解next数组的形式很多,有的next数组时从0开始的,有的是从1开始的,有的next数组返回的是之前字符串的长度,有的返回是的之前字符串的下标,不过根据我的理解就是,next数组最好从1开始,因为某些情况下,从0开始的next数组不符合需要!


下面举例说一下为什么:

字符串"aa",如果next数组从0开始的话,那么next[0]=-1 next[1]=0,如果next数组从1开始的话,那么next[0]=-1 next[1]=0 next[2]=1。

而对于字符串"ab",如果next数组从0开始的话,那么next[0]=-1 next[1]=0,如果next数组从1开始的话,那么next[0]=-1 next[1]=0 next[2]=0。

从上面可以看出,从0开始的话,无论第二个字母是什么,next[1]都是0,如果我需要求解这个字符串的对称的最大长度,那么从0开始的next数组显然不符合要求。


下面开始介绍next数组的作用以及如何求next数组,还是举字符串m为例,下面的图就是求next数组的过程,可以看出字符串m的前后有两部分是对称的,这就是自匹配,对于字符串m的每一个位置求next的值,如果字符串n和字符串m匹配的时候,在某个位置失配,利用next数组,下一次就不需要在m的开始位置重新匹配





下面看一下字符串n和字符串m匹配中的一环,长的字符串为n,短的是m,红色部分为失配位置,可以看出利用next数组,在n和m失配的时候,下一次匹配,m就不需要从头开始与n匹配。




求解next数组的重点就是继承

a 、当前面字符的前一个字符的对称程度为 0 的时候,只要将当前字符与子串第一个字符进行比较。这个很好理解啊,前面都是 0 ,说明都不对称了,如果多加了一个字符,要对称的话最多是当前的和第一个对称。比如 agcta 这个里面 t 的是 0 ,那么后面的 a 的对称程度只需要看它是不是等于第一个字符 a 了。



b 、按照这个推理,我们就可以总结一个规律,不仅前面是 0 呀,如果前面一个字符的 next 值是 1 ,那么我们就把当前字符与子串第二个字符进行比较,因为前面的是 1 ,说明前面的字符已经和第一个相等了,如果这个又与第二个相等了,说明对称程度就是 2 了。有两个字符对称了。比如上面 agctag ,倒数第二个 a 的 next 是 1 ,说明它和第一个 a 对称了,接着我们就把最后一个 g 与第二个 g 比较,又相等,自然对称成都就累加了,就是 2 了。  


c 、按照上面的推理,如果一直相等,就一直累加。


当然不可能会那么顺利让我们一直对称下去,如果遇到下一个不相等了,那么说明不能继承前面的对称性了,这种情况只能说明没有那么多对称了,但是不能说明一点对称性都没有,所以遇到这种情况就要重新来考虑,这个也是难点所在。

如果不相同,用一句话来说,就是:


从前面来找子前后缀


1 、如果要存在对称性,那么对称程度肯定比前面这个的对称程度小,所以要找个更小的对称,这个不用解释了吧,如果大那么就继承前面的对称性了。


2 、要找更小的对称,必然在对称内部还存在子对称,而且这个必须紧接着在子对称之后。


 

就像上面这张图,红色部分是当前的求解位置,如果红色部分和黄色部分的第一个字母不相等,则没有办法继承(蓝+绿+蓝)这部分的长度,但是却可以继承更小的长度,就是红色部分前面的一小部分蓝色


下面给出kmp的模板


//这里的str数组从下表0开始,如果希望从1开始则需要修改一下kmp函数 
char str[1000005],str2[10005];
int nextArr[10005];
int T,m,n;

void count_nextArr(){
	int j=-1,i=0;
	nextArr[0]=-1;
	while(i<m){
		if(j==-1 || str2[i]==str2[j]){
			nextArr[++i]=++j;
		} else{
			j=nextArr[j];
		}
	}
}

int kmp(){
	int i=0,j=0;
	while(i<n && j<m){
		if(str[i] == str2[j]){
			i++,j++;
		} else {
			if(j==0) i++;
			else j = nextArr[j];
		}
	}
	return (j==m) ? i-m+1 : -1;
}


hdu1711:模板题

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define INF 0x3f3f3f3f
using namespace std;
int a[1000005],b[10005],nextArr[10005];
int T,m,n;

void count_nextArr(){
	int j=-1,i=0;
	nextArr[0]=-1;
	while(i<m){
		if(j==-1 || b[i]==b[j]){
			nextArr[++i]=++j;
		} else{
			j=nextArr[j];
		}
	}
}

int kmp(){
	int i=0,j=0;
	while(i<n && j<m){
		if(a[i] == b[j]){
			i++,j++;
		} else {
			if(j==0) i++;
			else j = nextArr[j];
		}
	}
	return (j==m) ? i-m+1 : -1;
}

int main()
{
	//freopen("input.txt","r",stdin);
	cin>>T;
	while(T--){
		scanf("%d%d",&n,&m);
		for(int i=0;i<n;i++){
			scanf("%d",&a[i]);
		}
		for(int j=0;j<m;j++){
			scanf("%d",&b[j]);
		}
		count_nextArr();
		printf("%d\n",kmp());
	} 
	return 0;
}


hdu2203:kmp入门问题

只需变换一下,将原字符串变成两倍长再和模式串进行匹配,就可以遍历所有的轮回串。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
#define INF 0x3f3f3f3f
using namespace std;
char str[200005];
char str2[100005];
int nextArr[100005];
int n,m;

void count_nextArr(){
	int j=-1,i=0;
	nextArr[0]=-1;
	while(i<m){
		if(j==-1 || str2[i]==str2[j]){
			nextArr[++i]=++j;
		} else{
			j=nextArr[j];
		}
	}
}

void kmp(){
	int i=0,j=0;
	while(i<2*n && j<m){
		if(str[i] == str2[j]){
			i++,j++;
		} else{
			if(j==0){
				i++;
			} else{
				j = nextArr[j];
			}
		}
	}
	if(j!=m){
		printf("no\n");
	} else{
		printf("yes\n");
	}
} 

int main()
{
	//freopen("input.txt","r",stdin);
	while(~scanf("%s",str)) {
		scanf("%s",str2);
		n = strlen(str);
		m = strlen(str2);
		for(int i=n,j=0;i<2*n;i++,j++){
			str[i] = str[j];
		} 
		str[2*n] = '\0';
		count_nextArr();
		kmp();
	}
	
	return 0;
}


hdu2594:kmp入门问题

两个字符串s1和s2, 求出是s1的前缀并且是s2的后缀的最长的字符串。
连接两个字符串,例如next数组的特性求解即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<cstdlib>
#include<algorithm>
#define INF 0x3f3f3f3f
using namespace std;
char str[50005],str2[50005];
int nextArr[50005];
int n,m;

void count_nextArr(){
	int j=-1,i=0;
	nextArr[0]=-1;
	while(i<m+n){
		if(j==-1 || str[i]==str[j]){
			nextArr[++i]=++j;
		} else{
			j=nextArr[j];
		}
	}
}

int main()
{
//	freopen("input.txt","r",stdin);
	while(~scanf("%s",str)){
		scanf("%s",str2);
		n=strlen(str);
		m=strlen(str2);
		strcat(str,str2);
		count_nextArr();
		int len=m+n;
		while(nextArr[len]>m || nextArr[len]>n){
			len=nextArr[len];
		}
		for(int i=0;i<nextArr[len];i++){
			printf("%c",str[i]);
		}
		if(nextArr[len]) printf(" ");
		printf("%d\n",nextArr[len]);
	}
	return 0;
}



如果还想做更多的kmp题目,推荐看一下这个人的博客,列举了一些kmp中级题目

http://www.cnblogs.com/wuyiqi/archive/2012/01/05/2313746.html












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值