KMP&扩展KMP学习笔记(多图预警)

震惊,KMP加上扩展KMP的学习笔记字数竟然破万了(令人窒息)

KMP部分

例子:一个文本串A,一个模式串B,A的长度为n,B的长度为m,求B在A中出现的位置。(n,m<=106n,m<=10^6
题目链接:洛咕3375 【模板】kmp字符串匹配
暴力:枚举文本串中的位置ii,暴力比较A的[i,i+m1][i,i+m-1]这个区间是否与B相同。时间复杂度最坏情况是A,B都只有一种字符(比如A是aaaaa,B是aaa),此时时间复杂度为O(nm)O(nm)
燃鹅n,m<=106n,m<=10^6,所以要优化到线性。

发现暴力比较的过程中有很多冗余的操作,所以考虑优化这个过程。

一、next数组

next[i]next[i]表示模式串B中,假设前缀[1,i][1,i]构成的字符串为aa,使aa的前缀与后缀相同的最大长度,不算前、后缀为aa本身的情况(也就是规定next[i]<inext[i]<i)。
其中字符串ss的前缀[1,i][1,i]表示s1s2...sis_1s_2...s_i,后缀[i,n][i,n]表示sisi+1...sns_is_{i+1}...s_n

例子:字符串为ABABA。
珂以得出,next[1]=0,next[2]=0,next[3]=1,next[4]=2,next[5]=3next[1]=0,next[2]=0,next[3]=1,next[4]=2,next[5]=3
next[1]:考虑的是"A""A"。因为前、后缀不能取整个字符串,所以next[1]=0
next[2]:考虑的是"AB""AB"。因为"A""A"不等于"B""B",所以next[2]=0
next[3]:考虑的是"ABA""ABA"。最长前、后缀相等的长度为1,此时前缀为"A""A",后缀为"A""A"。 前、后缀相等的长度不能为2,因为长度为2的前缀为"AB""AB",后缀为"BA"。
next[4]:考虑的是"ABAB""ABAB"。前、后缀长度均为2时,前、后缀均为"AB""AB"。前、后缀长度为3时,前缀为"ABA""ABA",后缀为"BAB""BAB"
next[5]:考虑的是"ABABA""ABABA"。同理珂得,使前、后缀相同的最大长度为3,即前后缀均为"ABA""ABA"

求出next数组的方式:
让B自己与自己比较
比如B="ABABA"B="ABABA",现在要求出它的next[5]。
由于不能前、后缀为整个字符串,所以先把第二个B串往右移一格:

ABABA
 ABABA

忽略空出的部分,那么珂以发现,比较的是第一个B串的后缀"BABA""BABA",和第二个B串的"ABAB""ABAB"
发现并不相同,所以再把第二个B串往右移一格:

ABABA
  ABABA

同样地比较两串都非空的部分,即比较A串的后缀"ABA"和B串的前缀"ABA"。
因为后缀和前缀相同,所以next[5]=3。

正确性证明:
让B串和自己比较,把第二个B串往右移动一格,那么非空部分就分别表示第一个B串的后缀和第二个B串的前缀,然后让第一个B串的后缀和第二个B串的前缀比较。
如果第一个B串的后缀和第二个B串的前缀相同,那么表示这个长度是最大的能让B串的前后缀相同的长度。

然鹅这样仍然不是线性,所以还要优化:
假设对于一个字符串,需要求出next[i]next[i]的值。
考虑一个一个把字符加进去,那么现在已经加入了前i1i-1个字符(如图所示)。
nextnext数组的定义,这个字符串的前next[i1]next[i-1]个字符和后next[i1]next[i-1]个字符相同(如图所示)。
在这里插入图片描述
然后加入第ii个字符,如图,蓝色方框表示第ii个字符。令j=next[i1]j=next[i-1]
在这里插入图片描述
情况1:第j+1j+1个字符与第ii个字符相同
因为next[i1]next[i-1]已经是让前i1i-1个字符的前、后缀相同的最大长度,
因为j=next[i1]j=next[i-1],所以若第j+1j+1个字符与第ii个字符相同,则next[i]=j+1next[i]=j+1
证明:若存在比j+1j+1更长的长度,使前ii个字符前、后缀相同,那么next[i1]next[i-1]不是最大长度,所以矛盾。
因此这样有正确性qwq。
在这里插入图片描述
情况2:第j+1j+1个字符与第ii个字符不相同
然后考虑一个孙臭的情况:第j+1j+1个字符和第ii个字符不同。
这种情况j+1j+1不符合(因为前后缀不一样)。
珂以证明,最长的长度一定不超过jj
若有比jj更长的长度使前ii个字符的前后缀相同,则假设长度为kk
根据nextnext数组的定义,前kk个字符与后kk个字符相同,那么珂以推出前k1k-1个字符与倒数第kk个字符至第i1i-1个字符相同,所以next[i1]=k1next[i-1]=k-1(因为k>jk>j),与next[i1]=jnext[i-1]=j矛盾。
因此next[i]jnext[i]\le j

所以我们需要从11jj中找到一个长度,使得ii个字符中,前kk个字符和后kk个字符相同。
不妨去掉“前kk个字符”与“后kk个字符”两者的最后一个字符,即前k1k-1个字符与倒数第kk个字符至第i1i-1个字符分别相等(如图所示)。
在这里插入图片描述
所以,前jj个字符中,长度为k1k-1的前缀、后缀相等
next数组的定义:next[j]next[j]表示使前jj个字符前缀、后缀相等的最大长度。
所以此时我们让j=kj=k,然后检验第k+1k+1个字符是否与ii相等。
如果相等那么回到情况1,否则回到情况2。
代码实现:

int j=0;
for(int i=2; i<=n; i++) {	//next[1]=0
	//此时j存储的使next[i-1]的值 
	while(j>0 && str[j+1]!=str[i]) {	//注意判断j>0 
		//若第j+1个字符不等于第i个字符 
		//那么j+1不能使前i个字符前后缀相同,应继续循环(重复情况2) 
		j=nxt[j];
	}
	//判断,如果第j+1个字符与第i个字符相同,那么next[i]=j+1 
	if(str[j+1]==str[i])	j++;
	nxt[i]=j;
}

二、求出B在A中的位置

燃鹅仅知道一个字符串的nextnext数组是布星的,再回顾问题:
一个文本串A,一个模式串B,A的长度为n,B的长度为m,求B在A中出现的位置。(n,m<=106n,m<=10^6
同样遍历A,假设当前遍历到字符串A的第ii个字符,且A的前i1i-1个字符的后jj个字符与B的前jj个字符相同。
假设不存在更大的jj,使得A的后jj位与B的前jj位相同。
在这里插入图片描述
如图,若B串的第j+1j+1个字符与A串的第ii个字符相同,那么现在B就匹配到了第j+1j+1个字符。(不珂能匹配到更多字符,证明过程类似求nextnext数组中的情况1,这里不写了)

若第j+1j+1个字符与第ii个字符不相同,B串就不能匹配j+1j+1位。因此现在需要求出在A串加入第ii个字符后,B串与A串的后缀最多匹配几位。
首先可以知道能匹配的字符数不会超过j+1j+1(证明过程类似求nextnext的情况1)。
所以应该把B数组往右移。
在这里插入图片描述
假设移到如图所示的位置时,A串的后kk位与B串的前kk位相同。
那么我们珂以发现,新的B串(图中的new B)的前k1k-1个字符,和原B串的前jj个字符的后k1k-1个字符重合了。
而因为新的B串和原B串相同,所以B串的前jj个字符的前k1k-1个和后k1k-1个字符相同。
考虑nextnext的定义:next[i]next[i]表示前ii个字符的前后缀相同的最大长度。
因此k1k-1的最大值为next[j]next[j]
但是还需要保证B串的第kk个字符与A串的第ii个字符相同。
所以类似求nextnext数组的过程,每次让jj跳到next[j]next[j]的位置,然后判B串第next[j]+1next[j]+1个字符是否与A串第ii个字符相等即珂。

代码实现:

for(int i=1; i<=n; i++) {
	//跳到第一个B串的第j+1个字符与A[i]相等的位置(或跳到0,此时表示最大的匹配长度为0) 
	while(j>0 && B[j+1]!=A[i])	j=nxt[j];
	if(B[j+1]==A[i])	j++;
	if(j==m) {
		printf("%d\n",i-m+1);	//输出B在A串中的起始位置 
		j=nxt[j];	//j已经匹配到最后一位,所以重新开始匹配 
	}
}

(可能出现的)疑问

Q:以第二部分求B在A中的位置为例,每次都是把jj跳到next[j]next[j]的位置,也就是说,jj会先后变为next[j],next[next[j]],......next[j],next[next[j]],...... 那么会不会错过一些本来能使B的前j+1j+1个字符与A的后j+1j+1个字符成立的jj
A:不会。错过本来能使B的前j+1j+1个字符与A的后j+1j+1个字符成立的jj,意味着错过使B前后缀相等的长度jj
可以证明,next[j]next[j]是最大的使前jj个字符前后缀相等的长度,next[next[j]]next[next[j]]是第二大的使前jj个字符前后缀相等的长度,next[next[next[j]]next[next[next[j]]是……
过程如下:
由定义,next[j]next[j]是最大的使前jj个字符前后缀相等的长度,没毛病qwq。
假设第二长的使前jj个字符前后缀相同的长度不是next[next[j]]next[next[j]],而是比next[next[j]]next[next[j]]长的长度(如图中红线所示)。
在这里插入图片描述
假设红线长度为L(L>next[next[j]])L(L>next[next[j]])。因为红线与前next[j]next[j]个字符的前LL个、后LL个字符均相等,所以next[next[j]]next[next[j]]应为LL,矛盾。
所以比next[next[j]]next[next[j]]大的LL是不存在的qwq
因此next[next[j]]next[next[j]]是第二大的使……(不想打了)的长度。
同理next[next[next[j]]]next[next[next[j]]]是……(懒qwq)
这说明从jj开始不断取nextnext相当于从大到小遍历让前后缀相同的长度,故一定能取到这些长度中最大的一个qwq。

毒瘤代码

//Luogu P3375
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
using namespace std;
typedef long long ll;
int read() {
	re x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9') {
		x=10*x+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int Size=1000005;
int n,m,nxt[Size];
char A[Size],B[Size];
int main() {
	scanf("%s",A+1);
	scanf("%s",B+1);
	n=strlen(A+1);
	m=strlen(B+1);
	int j=0;
	for(re i=2; i<=n; i++) {
		//此时j存储的使next[i-1]的值 
		while(j>0 && B[j+1]!=B[i]) {	//注意判断j>0 
			//若第j+1个字符不等于第i个字符 
			//那么j+1不能使前i个字符前后缀相同,应继续循环(重复情况2) 
			j=nxt[j];
		}
		//判断,如果第j+1个字符与第i个字符相同,那么next[i]=j+1 
		if(B[j+1]==B[i])	j++;
		nxt[i]=j;
	}
	for(re i=1; i<=n; i++) {
		//跳到第一个B串的第j+1个字符与A[i]相等的位置(或跳到0,此时表示最大的匹配长度为0) 
		while(j>0 && B[j+1]!=A[i])	j=nxt[j];
		if(B[j+1]==A[i])	j++;
		if(j==m) {
			printf("%d\n",i-m+1);	//输出B在A串中的起始位置 
			j=nxt[j];	//j已经匹配到最后一位,所以重新开始匹配 
		}
	}
	for(re i=1; i<=m; i++)	printf("%d ",nxt[i]);
	return 0;
}

常用推论

推论1.next[next[i]]next[next[i]]表示第二大的前ii个字符的前后缀相同的长度,next[next[next[i]]]next[next[next[i]]]表示第三大的使……的长度
证明:上面已证。

推论2.用若干个串拼在一起(不重叠)把整个字符串覆盖,这样的串的长度最小为nnext[n]n-next[n]
证明:若可以找到更小的长度覆盖整个字符串,则可以证明next[n]next[n]应该更大。图略。

扩展KMP部分

网上的题解大多是下标从0开始,看着不刁惯并且难受……
推荐一个讲得很好的博客(不过下表是从0开始的):传送门

给定长度为nn的串SS,长度为mm的串TT,令S[a,b]S[a,b]表示SaSa+1Sa+2...SbS_aS_{a+1}S_{a+2}...S_bT[a,b]T[a,b]同理。
extend[i]extend[i]表示TTS[i,n]S[i,n]的最长公共前缀,next[i]next[i]表示TTT[i,m]T[i,m]的最长公共长度。
(即extendextendTT串与SS的第ii位之后的串的最长公共前缀,nextnextTT串与TT的第ii位之后的串的最长公共前缀,注意nextnext的定义发生了改变)

题外话:为什么这个看起来奇怪的东西叫扩展kmp呢?因为当extend[i]=mextend[i]=m时,TT就相当于在SS中出现了……

规定:
为了方便(和写代码的时候不发生奇怪的变量重名),extendextend写作extextnextnext写作nxtnxt
S[i]S[i]表示SS的第ii位,T[i]T[i]同理。

暴莉求extendextend显然是O(nm)O(nm)的(同kmpkmp的暴莉),考虑优化。
假设现在已经求出了ext[1]ext[1]ext[i1]ext[i-1],现在要求ext[i]ext[i](先不管nxtnxt数组怎么求)。
假设之前让TT与所有的S[i,n]S[i,n]匹配时,SS串匹配到的最远位置为PP,且最远位置是从pospos匹配到的。
也就是说,P=pos+ext[pos]1P=pos+ext[pos]-1,且S[pos,P]=T[1,Ppos+1]S[pos,P]=T[1,P-pos+1]

观察此图,发现S[i,P]=T[ipos+1,Ppos+1]S[i,P]=T[i-pos+1,P-pos+1]
而现在我们需要求出S[i,n]S[i,n]T[1,m]T[1,m]的最长公共前缀。
根据nextnext数组的定义,nxt[i]nxt[i]表示T[i,m]T[i,m]T[1,m]T[1,m]的最长公共前缀。
len=nxt[ipos+1]len=nxt[i-pos+1],然后分类讨论:

一、i+len1<=P\small i+len-1<=P

len=nxt[ipos+1]len=nxt[i-pos+1]表示T[1,len]=T[ipos+1,ipos+len]T[1,len]=T[i-pos+1,i-pos+len],因为i+len1i+len-1PP左边。
那么i+len1<=Pi+len-1<=P表示SS串中S[i,i+len1]=T[ipos+1,ipos+len]=T[1,len]S[i,i+len-1]=T[i-pos+1,i-pos+len]=T[1,len](如图)。
在这里插入图片描述
此时ext[i]=lenext[i]=len,因为
(1)S[i,i+len1]=T[1,len]S[i,i+len-1]=T[1,len]
(2)若ext[i]>lenext[i]>len,则说明T[len+1]=T[ipos+len+1]T[len+1]=T[i-pos+len+1],则nxt[ipos+1]>lennxt[i-pos+1]>len,与nxtnxt的定义不符。
因此ext[i]=lenext[i]=len

二、i+len1>P\small i+len-1>P

在这里插入图片描述
如图,S[P+1,i+len1]S[P+1,i+len-1]是一段没有比较过的位置,无法确定与TT是否相等。
S[i,P]=T[1,Pi+1]S[i,P]=T[1,P-i+1],所以从SS的第P+1P+1位,TT的第Pi+2P-i+2位开始暴力匹配,失配时表示这个长度是ext[i]ext[i]的值。

讲到这里,读者应该能写出求解extext数组的方法。
我的代码写得比较毒瘤,仅供参考qwq

void GetExtend() {
	int j=1;
	while(j<=n && j<=m && s[j]==t[j])	j++;
	ext[1]=j-1;
	int pos=1;
	for(re i=2; i<=n; i++) {
		//这个地方比较玄学,不能写i+nxt[i-pos+1]-1<=pos+ext[pos]-1
		if(i+nxt[i-pos+1]<=pos+ext[pos]-1) {
			ext[i]=nxt[i-pos+1];
		} else {
			j=max(pos+ext[pos],i);
			while(j<=n && j-i+1<=m && s[j]==t[j-i+1]) {
				j++;
			}
			ext[i]=j-i;
			pos=i;
		}
	}
}

求解nxtnxt数组的方法比较类似。因为nxt[i]nxt[i]表示的是T[i,m]T[i,m]TT的最长公共前缀,而ext[i]ext[i]表示的是S[i,m]S[i,m]TT的最长公共前缀,所以求nxtnxt时就让TTTT本身执行求extext的过程即珂。
由于求nxt[i]nxt[i]过程中,需要用到的nxtnxt的下标都比ii小,所以不会出现调用没有被求出的nxtnxt值。

void GetNext() {
	nxt[1]=m;
	int j=1;
	while(j<m && t[j]==t[j+1])	j++;
	nxt[2]=j-1;
	int pos=2;
	for(re i=3; i<=m; i++) {
		if(i+nxt[i-pos+1]<=pos+nxt[pos]-1) {
			nxt[i]=nxt[i-pos+1];
		} else {
			j=max(pos+nxt[pos],i);
			while(j<=m && t[j]==t[j-i+1]) {
				j++;
			}
			nxt[i]=j-i;
			pos=i;
		}
	}
}

毒瘤代码

输出extendextend数组:

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
using namespace std;
typedef long long ll;
int read() {
	re x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9') {
		x=10*x+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int Size=100005;
int n,m,nxt[Size],ext[Size];
char s[Size],t[Size];
void GetNext() {
	nxt[1]=m;
	int j=1;
	while(j<m && t[j]==t[j+1])	j++;
	nxt[2]=j-1;
	int pos=2;
	for(re i=3; i<=m; i++) {
		if(i+nxt[i-pos+1]<=pos+nxt[pos]-1) {
			nxt[i]=nxt[i-pos+1];
		} else {
			j=max(pos+nxt[pos],i);
			while(j<=m && t[j]==t[j-i+1]) {
				j++;
			}
			nxt[i]=j-i;
			pos=i;
		}
	}
}
void GetExtend() {
	int j=1;
	while(j<=n && j<=m && s[j]==t[j])	j++;
	ext[1]=j-1;
	int pos=1;
	for(re i=2; i<=n; i++) {
		if(i+nxt[i-pos+1]<=pos+ext[pos]-1) {
			ext[i]=nxt[i-pos+1];
		} else {
			j=max(pos+ext[pos],i);
			while(j<=n && j-i+1<=m && s[j]==t[j-i+1]) {
				j++;
			}
			ext[i]=j-i;
			pos=i;
		}
	}
}
int main() {
//	freopen("data.txt","r",stdin);
//	freopen("WA.txt","w",stdout);
	scanf("%s",s+1);
	scanf("%s",t+1);
	n=strlen(s+1);
	m=strlen(t+1);
	GetNext();
	GetExtend();
	for(re i=1; i<=n; i++) {
		printf("%d ",ext[i]);
	}
	return 0;
}
/*
dadab
dad
*/

例题

还没写题,待续qwq

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章