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

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