震驚,KMP加上擴展KMP的學習筆記字數竟然破萬了(令人窒息)
KMP部分
例子:一個文本串A,一個模式串B,A的長度爲n,B的長度爲m,求B在A中出現的位置。()
題目鏈接:洛咕3375 【模板】kmp字符串匹配
暴力:枚舉文本串中的位置,暴力比較A的這個區間是否與B相同。時間複雜度最壞情況是A,B都只有一種字符(比如A是aaaaa,B是aaa),此時時間複雜度爲。
燃鵝,所以要優化到線性。
發現暴力比較的過程中有很多冗餘的操作,所以考慮優化這個過程。
一、next數組
令表示模式串B中,假設前綴構成的字符串爲,使的前綴與後綴相同的最大長度,不算前、後綴爲本身的情況(也就是規定)。
其中字符串的前綴表示,後綴表示
例子:字符串爲ABABA。
珂以得出,。
next[1]:考慮的是。因爲前、後綴不能取整個字符串,所以next[1]=0
next[2]:考慮的是。因爲不等於,所以next[2]=0
next[3]:考慮的是。最長前、後綴相等的長度爲1,此時前綴爲,後綴爲。 前、後綴相等的長度不能爲2,因爲長度爲2的前綴爲,後綴爲"BA"。
next[4]:考慮的是前、後綴長度均爲2時,前、後綴均爲。前、後綴長度爲3時,前綴爲,後綴爲。
next[5]:考慮的是。同理珂得,使前、後綴相同的最大長度爲3,即前後綴均爲。
求出next數組的方式:
讓B自己與自己比較:
比如,現在要求出它的next[5]。
由於不能前、後綴爲整個字符串,所以先把第二個B串往右移一格:
ABABA
ABABA
忽略空出的部分,那麼珂以發現,比較的是第一個B串的後綴,和第二個B串的。
發現並不相同,所以再把第二個B串往右移一格:
ABABA
ABABA
同樣地比較兩串都非空的部分,即比較A串的後綴"ABA"和B串的前綴"ABA"。
因爲後綴和前綴相同,所以next[5]=3。
正確性證明:
讓B串和自己比較,把第二個B串往右移動一格,那麼非空部分就分別表示第一個B串的後綴和第二個B串的前綴,然後讓第一個B串的後綴和第二個B串的前綴比較。
如果第一個B串的後綴和第二個B串的前綴相同,那麼表示這個長度是最大的能讓B串的前後綴相同的長度。
然鵝這樣仍然不是線性,所以還要優化:
假設對於一個字符串,需要求出的值。
考慮一個一個把字符加進去,那麼現在已經加入了前個字符(如圖所示)。
由數組的定義,這個字符串的前個字符和後個字符相同(如圖所示)。
然後加入第個字符,如圖,藍色方框表示第個字符。令。
情況1:第個字符與第個字符相同
因爲已經是讓前個字符的前、後綴相同的最大長度,
因爲,所以若第個字符與第個字符相同,則。
證明:若存在比更長的長度,使前個字符前、後綴相同,那麼不是最大長度,所以矛盾。
因此這樣有正確性qwq。
情況2:第個字符與第個字符不相同
然後考慮一個孫臭的情況:第個字符和第個字符不同。
這種情況不符合(因爲前後綴不一樣)。
珂以證明,最長的長度一定不超過:
若有比更長的長度使前個字符的前後綴相同,則假設長度爲。
根據數組的定義,前個字符與後個字符相同,那麼珂以推出前個字符與倒數第個字符至第個字符相同,所以(因爲),與矛盾。
因此。
所以我們需要從到中找到一個長度,使得個字符中,前個字符和後個字符相同。
不妨去掉“前個字符”與“後個字符”兩者的最後一個字符,即前個字符與倒數第個字符至第個字符分別相等(如圖所示)。
所以,前個字符中,長度爲的前綴、後綴相等。
next數組的定義:表示使前個字符前綴、後綴相等的最大長度。
所以此時我們讓,然後檢驗第個字符是否與相等。
如果相等那麼回到情況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中的位置
燃鵝僅知道一個字符串的數組是布星的,再回顧問題:
一個文本串A,一個模式串B,A的長度爲n,B的長度爲m,求B在A中出現的位置。()
同樣遍歷A,假設當前遍歷到字符串A的第個字符,且A的前個字符的後個字符與B的前個字符相同。
假設不存在更大的,使得A的後位與B的前位相同。
如圖,若B串的第個字符與A串的第個字符相同,那麼現在B就匹配到了第個字符。(不珂能匹配到更多字符,證明過程類似求數組中的情況1,這裏不寫了)
若第個字符與第個字符不相同,B串就不能匹配位。因此現在需要求出在A串加入第個字符後,B串與A串的後綴最多匹配幾位。
首先可以知道能匹配的字符數不會超過(證明過程類似求的情況1)。
所以應該把B數組往右移。
假設移到如圖所示的位置時,A串的後位與B串的前位相同。
那麼我們珂以發現,新的B串(圖中的new B)的前個字符,和原B串的前個字符的後個字符重合了。
而因爲新的B串和原B串相同,所以B串的前個字符的前個和後個字符相同。
考慮的定義:表示前個字符的前後綴相同的最大長度。
因此的最大值爲!
但是還需要保證B串的第個字符與A串的第個字符相同。
所以類似求數組的過程,每次讓跳到的位置,然後判B串第個字符是否與A串第個字符相等即珂。
代碼實現:
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中的位置爲例,每次都是把跳到的位置,也就是說,會先後變爲 那麼會不會錯過一些本來能使B的前個字符與A的後個字符成立的?
A:不會。錯過本來能使B的前個字符與A的後個字符成立的,意味着錯過使B前後綴相等的長度。
可以證明,是最大的使前個字符前後綴相等的長度,是第二大的使前個字符前後綴相等的長度,是……
過程如下:
由定義,是最大的使前個字符前後綴相等的長度,沒毛病qwq。
假設第二長的使前個字符前後綴相同的長度不是,而是比長的長度(如圖中紅線所示)。
假設紅線長度爲。因爲紅線與前個字符的前個、後個字符均相等,所以應爲,矛盾。
所以比大的是不存在的qwq
因此是第二大的使……(不想打了)的長度。
同理是……(懶qwq)
這說明從開始不斷取相當於從大到小遍歷讓前後綴相同的長度,故一定能取到這些長度中最大的一個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.表示第二大的前個字符的前後綴相同的長度,表示第三大的使……的長度。
證明:上面已證。
推論2.用若干個串拼在一起(不重疊)把整個字符串覆蓋,這樣的串的長度最小爲。
證明:若可以找到更小的長度覆蓋整個字符串,則可以證明應該更大。圖略。
擴展KMP部分
網上的題解大多是下標從0開始,看着不刁慣並且難受……
推薦一個講得很好的博客(不過下表是從0開始的):傳送門
給定長度爲的串,長度爲的串,令表示,同理。
令表示與的最長公共前綴,表示與的最長公共長度。
(即是串與的第位之後的串的最長公共前綴,爲串與的第位之後的串的最長公共前綴,注意的定義發生了改變)
題外話:爲什麼這個看起來奇怪的東西叫擴展kmp呢?因爲當時,就相當於在中出現了……
規定:
爲了方便(和寫代碼的時候不發生奇怪的變量重名),寫作,寫作。
表示的第位,同理。
暴莉求顯然是的(同的暴莉),考慮優化。
假設現在已經求出了到,現在要求(先不管數組怎麼求)。
假設之前讓與所有的匹配時,串匹配到的最遠位置爲,且最遠位置是從匹配到的。
也就是說,,且。
觀察此圖,發現。
而現在我們需要求出與的最長公共前綴。
根據數組的定義,表示與的最長公共前綴。
設,然後分類討論:
一、
表示,因爲在左邊。
那麼表示串中(如圖)。
此時,因爲
(1)
(2)若,則說明,則,與的定義不符。
因此。
二、
如圖,是一段沒有比較過的位置,無法確定與是否相等。
而,所以從的第位,的第位開始暴力匹配,失配時表示這個長度是的值。
講到這裏,讀者應該能寫出求解數組的方法。
我的代碼寫得比較毒瘤,僅供參考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;
}
}
}
求解數組的方法比較類似。因爲表示的是與的最長公共前綴,而表示的是與的最長公共前綴,所以求時就讓和本身執行求的過程即珂。
由於求過程中,需要用到的的下標都比小,所以不會出現調用沒有被求出的值。
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;
}
}
}
毒瘤代碼
輸出數組:
#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