扩展KMP的详细理解

随笔 专栏收录该内容
39 篇文章 1 订阅

扩展KMP的详细理解

扩展KMP求的是对于原串S1的每一个后缀子串与模式串S2的最长公共前缀。它有一个next[]数组和一个extend[]数组。

next[i]表示为模式串S2中以i为起点的后缀字符串和模式串S2的最长公共前缀长度.

其中,next[0]=l2;

next[i]=max{ k|i<=i+k-1<l2 &&S2.substring(i,i+k-1) == S2.substring(0,k-1) }

其中str.substring(i, j)表示str从位置i到位置j的子串,如果i>j则,substring为空。

extend[i]表示为以字符串S1中以i为起点的后缀字符串和模式串S2的最长公共前缀长度.

下面我们先以一组样例来理解扩展KMP的过程


(1) 第一步,我们先对原串S1和模式串S2进行逐一匹配,直到发生不配对的情况。我们可以看到,S1[0]=S2[0],S1[1]=S2[1],S1[2]=S2[2],S1[3] ≠S2[3],此时匹配失败,第一步结束,我们得到S1[0,2]=S2[0,2],即extend[0]=3;

(2) Extend[0]的计算如第一步所示,那么extend[1]的计算是否也要从原串S1的1位置,模式串的0位置开始进行逐一匹配呢?扩展KMP优化的便是这个过程。从extend[0]=3的结果中,我们可以知道,S1[0,2]=S2[0,2],那么S1[1.2]=S2[1,2]。因为next[1]=4,所以S2[1,4]=S2[0,3],即S2[1,2]=S[0,1],可以得出S1[1,2]=S2[1,2]=S2[0,1],然后我们继续匹配,S1[3] ≠S2[3],匹配失败,extend[1]=2;

(3) 因为extend[1]=2,则S1[1,2]=S2[0,1],所以S1[2,2]=S2[0,0],因为next[0]=5,所以S1[0,5]=S2[0,5],所以S2[0,0]=S2[0,0],又回到S1[2,2]=S2[0,0],继续匹配下一位,因为S1[3] ≠S2[1],所以下一位匹配失败,extend[2]=1;

(4) 到计算原串S1的3号位置(在之前的步骤中能匹配到的最远的位置+1,即发生匹配失败的位置),这种情况下,我们会回到步骤(1)的方式,从原串S1的3号位置开始和模式串的0号位置开始,进行逐一匹配,直到匹配失败,此时的extend[]值即为它的匹配长度。因为S1[3] ≠S2[0],匹配失败,匹配长度为0,即extend[3]=0;

(5) 计算S1的4号位置extend[]。由于原串S1的4号位置也是未匹配过的,我们也是回到步骤(1)的方式,从原串S1的4号位置开始和模式串S2的0号位置开始进行逐一匹配,可以看到,S1[4]=S2[0],S1[5]=S2[1],S1[6]=S2[2],S1[7]=S2[3],S1[8]=S2[4],S1[9] ≠S2[5],此时原串S1的9号位置发生匹配失败,最多能匹配到原串S1的8号位置,即S1[4,8]=S2[0,4],匹配长度为5,即extend[4]=5;

(6) 计算S1的5号位置extend[].由于原串S1的5号位置是匹配过的(在步骤(5)中匹配了),我们从extend[4]=5得出,S1[4,8]=S2[0,4],即S1[5,8]=S2[1,4],和步骤(2)的计算方式类似,我们从next[1]=4可知,S2[0,3]=S2[1,4],即S1[5,8]=S2[0,3],然后我们继续匹配原串S1的9号位置和S2的4号位置,S1[9]=S2[4],继续匹配,S1[10]=S2[5],此时原串S1的所有字符皆匹配完毕,皆大欢喜,则S1[5,10]=S2[0,5],extend[5]=6;

(7) 从原串S1的6号位置到10号位置的extend[]的计算,与原串S1的1号位置到3号位置的计算基本相同。S1[6,10]=S2[1,5],因为next[1]=4,所以S2[1,4]=S[0,3],所以S1[6,9]=S2[0,3],此时不在需要判断匹配下一位的字符了,直接extend[6]=4;(具体原因在后面的分析总结中有说明)

(8) S1[7,10]=S2[2,5],因为next[3]=2,所以S2[3,4]=S2[0,1],所以S1[8,9]=S2[0,1],匹配长度为2,即extend[7]=3;

(9) S1[8,10]=S2[3,5],因为next[3]=2,所以S2[3,4]=S2[0,1],所以S1[8,9]=S2[0,1],匹配长度为2,即extend[8]=2;

(10) S1[9,10]=S2[4,5],因为next[4]=1,所以S2[4,5]=S2[0,0],所以S1[9,9]=S2[0,0],匹配长度为1,即extend[9]=1;

(11) S1[10,10]=S2[5,5],因为next[5]=0,所以匹配长度为0,即extend[10]=0;

至此,所有的匹配已经结束,相信,如果你仔细的看了上述的例子,已经对扩展KMP有了一定的了解了,它的计算过程中,主要是步骤一和步骤二的计算过程。下面我们对这两个过程归纳一下:

 

我们先定义,从0~k的计算过程中,我们已经计算出它们的extend[]值了和在匹配的过程中从Po开始匹配能匹配到的最远位置P,即Po+extend[Po]-1=P;

步骤一:当前需要计算的extend[k+1],原串S1中k+1号位置还未进行过匹配,则从原串S1的k+1号位置和模式串S2的0号位置开始进行逐一匹配,直到匹配失败,则extend[k+1]=匹配长度,此外,还要相应的更新Po值和最远匹配位置P.

步骤二:当前需要计算的extend[k+1],原串S1中k+1号位置已经进行过匹配。首先,我们从Po+extend[Po]-1=P中,可以得知S1[Po,P]=S2[0,P-Po],所以S1[k+1,P]=S2[k+1-Po,P-Po],令len=next[k+1-Po]

(1) 当(k+1)+len-1=k+len<P时,即一下情况:

 

我们可以得出,len=next[k+1-Po],S2[0,len-1]=S2[k+1-Po,k+Po+len],所以S1[k+1,k+len]=S2[k+1-Po,k+Po+len]=S2[0,len-1],即extend[k+1]=len;

那么会不会出现S1[k+len+1]=S2[len]的情况呢?答案是否定的

假如S1[k+len+1]=S2[len],则S1[k+1,k+len+1]=S2[0,len]

因为k+len<P,所以k+len+1<=P

所以S1[k+1,k+len+1]=S2[k+1-Po,k+Po+len+1]=S2[0,len]

此时,next[k+1-Po]=len+1与原假设不符合,所以此时S1[k+len+1]≠S2[len],不需要再次判断。

(2)当(k+1)+len-1=k+len>=P时,即一下情况:

 

我们可以看出,由S1[Po,P]=S2[0,P-Po]可得出S1[k+1,P]=S2[k+1-Po,P-po],len=next[k+1-Po],所以S2[0,len-1]=S2[k+1-Po,k+len+Po]

所以S1[k+1,p]=S2[0,P-k-1]

由于大于P的位置我们还未进行匹配,所以从原串S1的P+1位置开始和模式串的P-k位置开始进行逐一匹配,直到匹配失败,并更新相应的Po位置和最远匹配位置P,此时extend[k+1]=P-k+后来逐一匹配的匹配长度。

其实,next[]数组的计算过程与extend[]的计算过程基本一致,可以看成是原串S2和模式串S2的扩展KMP进行计算,每次计算extend[k+1]时,next[i](0<=i<=k)已经算出来了,算出extend[k+1]的时候,意味着next[k+1]=extend[k+1]也计算出来了。

时间复杂度分析

通过上面的算法可知,我们原串S1的每一个字符串只会进行一次匹配,extend[k+1]的计算可以通过之前extend[i](0<=i<=k)的值得出,由于需要用相同的方法对模式串S2进行一次预处理,所以扩展KMP的时间复杂度为O(l1+l2),其中,l1为原串S1的长度,l2为模式串S2的长度。

HDU - 2328 

Corporate Identity

题意:给你n个字符串,求这n个字符串的最长公共子串
思路:有多种方面可以做出这道题,我们这里先找出最短的一个母串,然后枚举它的每一个子串,对于每一个子串和原来的母串进行扩展KMP匹配,然后记录匹配的最大值和对应的位置即可,需要注意的时,多个答案时,输出字典序最小的子串。
//#include<bits/stdc++.h>
/*
next[0]=l2; 
next[i]=max{ k|i<=i+k-1<l2 &&str.substring(i,i+k-1) == str.substring(0,k-1) } 其中str.substring(i, j)表示str从位置i到位置j的子串,如果i>j则,substring为空。
next[i]表示为以模式串s2中以i为起点的后缀字符串和模式串s2的最长公共前缀长度.
extend[i]表示为以字串s1中以i为起点的后缀字符串和模式串s2的最长公共前缀长度.
*/
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#include<map>
using namespace std;
const int maxn=100010;   //字符串长度最大值
const int INF=int(1e9);
int nxt[maxn],ex[maxn]; //ex数组即为extend数组
char s1[maxn],s2[maxn];
char s[5010][210];
//预处理计算next数组
void get_next(char *str) {
	int i=0,j,po,len=strlen(str);
	nxt[0]=len;//初始化next[0]
	while(str[i]==str[i+1]&&i+1<len)//计算next[1]
		i++;
	nxt[1]=i;
	po=1;//初始化po的位置
	for(i=2; i<len; i++) {
		if(nxt[i-po]+i<nxt[po]+po)//第一种情况,可以直接得到next[i]的值
			nxt[i]=nxt[i-po];
		else { //第二种情况,要继续匹配才能得到next[i]的值
			j=nxt[po]+po-i;
			if(j<0)j=0;//如果i>po+next[po],则要从头开始匹配
			while(i+j<len&&str[j]==str[j+i])//计算next[i]
				j++;
			nxt[i]=j;
			po=i;//更新po的位置
		}
	}
}
//计算extend数组
bool exkmp(char *s1,char *s2) {
	int i=0,j,po,len=strlen(s1),l2=strlen(s2);
	get_next(s2);//计算子串的next数组
	while(s1[i]==s2[i]&&i<l2&&i<len)//计算ex[0]
		i++;
	ex[0]=i;
	po=0;//初始化po的位置
	if(ex[0]==l2)
		return true;
	for(i=1; i<len; i++) {
		if(nxt[i-po]+i<ex[po]+po)//第一种情况,直接可以得到ex[i]的值
			ex[i]=nxt[i-po];
		else { //第二种情况,要继续匹配才能得到ex[i]的值
			j=ex[po]+po-i;
			if(j<0)j=0;//如果i>ex[po]+po则要从头开始匹配
			while(i+j<len&&j<l2&&s1[j+i]==s2[j])//计算ex[i]
				j++;
			ex[i]=j;
			po=i;//更新po的位置
		}
		if(ex[i]==l2)
			return true;
	}
	return false;
}
void char_(char *str1,char *str,int l,int r) {    //将str字符串的(l,r)赋值给str1 
	for(int i=l; i<=r; i++) {
		str1[i-l]=str[i];
	}
	str1[r-l+1]='\0';
	return ;
}
int main() {
	int n;
	while(~scanf("%d",&n)&&n) {
		scanf("%d",&n);
		int Min=INF,pos;
		for(int i=1; i<=n; i++) {
			scanf("%s",s[i]);
			if(strlen(s[i])<Min) {    //找出最短母串 
				Min=strlen(s[i]);
				pos=i;
			}
		}
		int Max=0;
		int l=strlen(s[pos]);
		int left,right;
		for(int i=0; i<l; i++) {   //枚举母串的每一个子串 
			for(int j=i; j<l; j++) {
				if((j-i+1)<Max) continue;   //剪枝 
				char_(s1,s[pos],i,j);
				int flag=true;
				for(int z=1; z<=n; z++) {  //每一个子串和原来的母串进行匹配 
					if(z==pos) continue;
					if(exkmp(s[z],s1))
						continue;
					flag=false;
					break;
				}
				if(flag) {
					if(j-i+1==Max){      //记录所有母串的最长公共子串,长度相同的情况下记录字典序最小的子串 
						for(int z=0;z+left<=right;z++){
							if(s[pos][z+left]<s[pos][z+i])
							break;
							else{
								if(s[pos][z+left]>s[pos][z+i]){
									left=i;
									right=j;
									break;
								}
							}
						}
					}
					if(j-i+1>Max) {
						left=i;
						right=j;
						Max=max(Max,j-i+1);
					}
				}
			}
		}
		if(Max==0)
			printf("IDENTITY LOST\n");
		else {
			for(int i=left; i<=right; i++) {
				printf("%c",s[pos][i]);
			}
			printf("\n");
		}
	}
	return 0;
}

 

  • 6
    点赞
  • 4
    评论
  • 18
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页

打赏

brandong

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值