演算法詳解
KMP是一種字串匹配演算法,可以線上性的時間複雜度內解決字串的“模式定位”問題,即:
在字串 \(A\) 中查詢字串 \(B\) 出現的所有位置。
我們稱 \(A\) 為主串,\(B\) 為模式串。下文都用\(n\)表示\(A\)的長度,\(m\)表示\(B\)的長度,下標從\(1\)開始。
初始狀態,我們用兩個指標 \(i,j\) 分別指向 \(A\) 和 \(B\) 的第 \(1\) 位。
KMP的主過程如下(後面會證明它的正確性):
- 如果\(A[i]=B[j]\),則
i++
,j++
。 - 如果\(A[i]\neq B[j]\):
- 如果\(j>1\),則
j=nxt[j-1]+1
。 - 如果\(j\le 1\),則
i++
。
- 如果\(j>1\),則
我們定義\(nxt[0]=0\)。
其中,\(nxt\)陣列是一個用於表示發生失配後,\(j\)回退到的位置。具體求法待會會說,下面我們用一個例子來理解\(nxt\)的功能,我將\(nxt\)陣列畫在圖片的下方了。
- 初始狀態。\(i=1,j=1\)。
- \(A[i]=B[j]\),同時右移\(i,j\)。\(i=2,j=2\)。
- \(A[i]=B[j]\),同時右移\(i,j\)。\(i=3,j=3\)。
- \(A[i]=B[j]\),同時右移\(i,j\)。\(i=4,j=4\)。
- \(A[i]=B[j]\),同時右移\(i,j\)。\(i=5,j=5\)。
- \(A[i]=B[j]\),同時右移\(i,j\)。\(i=6,j=6\)。
- 發現\(A[i]\neq B[j]\),於是查表\(nxt[j-1]+1=nxt[5]+1=3\),於是\(j=3\)。此時\(i=6\)。
可以發現此時灰色箭頭指向的兩部分是相等的。
進一步來說,從上一張圖開始,下劃線的部分都是相等的。
- OK我們繼續,\(A[i]=B[j]\),於是\(i,j\)同時右移,\(i=7,j=4\)。
- 發現\(A[i]\neq B[j]\),查表得\(nxt[3]+1=2\),於是\(j=2\)。此時\(i=7\)。
我們又發現\(A\)與\(B\)的下劃線部分重合了,所以仍然可以從重合部分之後開始比較。
- 然後\(A[i]=B[j]\)……就一直比到結束了,就不放圖了w。
我們可以把根據上面的過程(KMP主過程)寫出程式碼(下標從\(1\)開始):
i=1,j=1;
while(i<=n){
if(a[i]==b[j]) i++,j++;
else if(j>1) j=nxt[j-1]+1;
else i++;
if(j==m+1) cout<<i-j+1<<"\n";
//輸出配對的開始位置,理解一下這裡
}
可以發現\(i\)是始終不降的,比起暴力,KMP透過避免\(i\)指標的回滾操作,減少了時間開銷。
到這裡我們已經可以初步理解\(nxt\)的含義,它是用來幫我們瞭解發生失配時,\(j\)應該回退到什麼位置。
再進一步分析,我們發現:\(nxt[x]\),其實就是\(B[1\sim x]\)這一段的最長公共前字尾長度。
舉個例子,YABAPQYABZABAAPYABAPQYABA。這是一個長度為\(24\)的字串,那麼當它作為\(B\)時:
- \(nxt[23]=9\),如你所見,紅色部分是\(B[1\sim 23]\)的公共前字尾,它的長度為\(9\)。
- \(nxt[9]=3\),因為
YAB
是\(B[1\sim 9]\)的公共前字尾,它的長度為\(3\)。 - \(nxt[3]=0\),因為\(B[1\sim 3]\)沒有公共前字尾。
- \(nxt[24]=0\),因為\(B[1\sim 24]\)沒有公共前字尾。
注意:公共前字尾可能有重疊部分,但是一個字串不能作為它自己的公共前字尾(嚴格來說,我們把這種前/字尾稱作真前/字尾,所以後面都帶“真”來稱呼了)。
我們規定,\(nxt[x]\)表示的必須是\(B[1\sim x]\)這一段的最長公共真前字尾長度,這麼定義是為了保證正確性,如果不規定最長,不能保證答案的正確性。後面的證明會提到。
現在我們的首要問題是,如何求\(nxt\)陣列?
我們假定\(i\)之前的答案都已經求出,現在需要求\(nxt[i]\)。
我們設\(j\)為\(nxt[i-1]+1\),如下圖:
最簡單的情況自然就是\(B[i]=B[j]\),那麼\(nxt[i]=j\),這是顯然的。
但是也可能遇到更棘手的情況:
\(B[i]\neq B[j]\),怎麼辦呢?
既然YABAPQYAB這個公共真前字尾不行,那就再找一個更小的!
顯然更小的前字尾就是YAB了,怎麼得來的呢?就是看\(nxt[j-1]\)的值是多少,那麼新的\(j\)就是\(nxt[j-1]+1\)。此時再看看\(B[i]=B[j]\)是否成立。成立的話就\(nxt[i]=j\),不成立就繼續找\(j\),直到找到\(B[i]=B[j]\),或者\(j=1\)為止。
可以發現我們充分利用了之前計算出的\(nxt\),將原規模逐步縮小,是不是有點歸納法的意味?
再來舉一個不斷更新\(j\)直到\(j=1\)為止的例子:
我們可以把這一步驟也寫成程式碼(下標從\(1\)開始):
int i=2,j=1;//nxt[1]應為0,所以i從2開始
while(i<=m){
if(b[i]==b[j]) nxt[i++]=j++;
else if(j>1) j=nxt[j-1]+1;
else nxt[i++]=0;
}
(是不是和上面的程式碼很像)
至此把兩個程式碼拼接起來就能得到KMP的全過程了。
P3375 【模板】KMP
下標從$1$開始
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N];
int main(){
cin>>a>>b;
n=a.size(),m=b.size();
a=' '+a,b=' '+b;
int i=2,j=1;
while(i<=m){
if(b[i]==b[j]) nxt[i++]=j++;
else if(j>1) j=nxt[j-1]+1;
else nxt[i++]=0;
}
i=1,j=1;
while(i<=n){
if(a[i]==b[j]) i++,j++;
else if(j>1) j=nxt[j-1]+1;
else i++;
if(j==m+1) cout<<i-j+1<<"\n";
}
for(int i=1;i<=m;i++) cout<<nxt[i]<<" ";
return 0;
}
下標從$0$開始
#include<bits/stdc++.h>
#define N 1000010
using namespace std;
string a,b;
int n,m,nxt[N];
int main(){
cin>>a>>b;
n=a.size(),m=b.size();
int i=1,j=0;
while(i<m){
if(b[j]==b[i]) nxt[i++]=++j;
else if(j) j=nxt[j-1];
else nxt[i++]=0;
}
i=0,j=0;
while(i<n){
if(a[i]==b[j]) i++,j++;
else if(j) j=nxt[j-1];
else i++;
if(j==m) cout<<i-j+1<<"\n";
}
for(int i=0;i<m;i++) cout<<nxt[i]<<" ";
return 0;
}
正確性證明
我們剛接觸\(nxt\)的定義時,可能會有疑問:為什麼\(nxt\)一定要表示\(B[1\sim x]\)這一段的最長公共真前字尾長度?
我們用反證法證明一下正確性,順帶解決這個問題。
根據KMP的流程,如果遇到失配的情況,我們先找\(B[1\sim (j-1)]\)中的最長公共真前字尾(如圖,用\(f\)表示)的長度\(nxt[j-1]\),然後把\(j\)置為\(nxt[j-1]+1\),如下圖。
要證明KMP是正確的,就是證明\(j\)回退時不會跳過正確答案。
我們假設在\(j>nxt[j-1]+1\)時存在匹配,如下圖,那麼灰色箭頭連線的部分都相等。
自然,黃色大括號部分(不包含\(i,j\))也相等,那麼黃色部分的長度\(=j-1\)。
回到第\(1\)張圖(\(j'\)表示第\(1\)張圖情況下的\(j\)),根據假設,\(j>|f|+1\),即黃色部分長度\(>|f|\)。
這樣的話,黃色部分就成了\(B[1\sim j']\)最長的公共真前字尾,與“\(f\)是\(B[1\sim j']\)最長公共真前字尾”矛盾。
得到的結論是:\(j>nxt[j-1]+1\)時一定不存在匹配。
也就是說\(j\)回退是不會漏掉解的,正確性得證。
這樣我們的問題也迎刃而解了:如果\(nxt\)不是最長,上面的情況就可能不形成矛盾,進而\(j>nxt[j-1]+1\)時也可能存在解,而這被我們忽略掉了,所以正確性不能保證。
複雜度證明
透過看程式碼能發現兩部分程式碼相似,所以就一塊說了:
\(i\)每增加\(1\),\(j\)最多也增加\(1\),從而\(j\)最多增加\(len\)次,進而最多減少\(len\)次。
所以處理\(nxt\)是\(O(m)\)的,主過程是\(O(n)\)的,總時間複雜度就是\(O(n+m)\)了。——字串學習筆記 · 淺析KMP——單模式串匹配演算法 - 皎月半灑花
\(\mathcal{NEXT\ \ \ PHANTASM...}\)