一、何謂模式串匹配
模式串匹配,就是給定一個需要處理的文本串(理論上應該很長)和一個需要在文本串中搜索的模式串(理論上長度應該遠小於文本串),查詢在該文本串中,給出的模式串的出現有無、次數、位置等。
模式串匹配的意義在於,如果我是一個平台的管理員,我可以針對一篇文章或者一句話,搜索其中某個特定臟字或者不雅詞匯的出現次數、位置——次數可以幫助我決定采取何種等級對於該用戶的懲罰方式,而位置則可以幫助我給每一個臟詞打上“*”的標記來自動屏蔽這些臟詞。
二、淺析 KMPKMP 之思想
哦呵呵這個算法的名字比較詭異是因為有三位偉大的科學家共同設計完成……分別是 \mathcal{Knuth(D.E.Knuth) \& Morris(J.H.Morris)\& Pratt(V.R.Pratt)}Knuth(D.E.Knuth)&Morris(J.H.Morris)&Pratt(V.R.Pratt)
然而我並不知道他們是誰
首先要理解,朴素的單模式串匹配大概就是枚舉每一個文本串元素,然后從這一位開始不斷向后比較,每次比較失敗之后都要從頭開始重新比對,大概期望時間復雜度在 \Theta(n+m)Θ(n+m) 左右,對於一般的弱數據還是闊以跑的了滴。但是其實是可以被卡成 O(nm)O(nm) 的。 emmmmemmmm 並且還是比較容易卡的。
而 KMPKMP 的精髓在於,對於每次失配之后,我都不會從頭重新開始枚舉,而是根據我已經得知的數據,從某個特定的位置開始匹配;而對於模式串的每一位,都有唯一的“特定變化位置”,這個在失配之后的特定變化位置可以幫助我們利用已有的數據不用從頭匹配,從而節約時間。
比如我們考慮一組樣例:
模式串:abcab
文本串:abcacababcab
首先,前四位按位匹配成功,遇到第五位不同,而這時,我們選擇將 abcababcab 向右移三位,或者可以直接理解為移動到模式串中與失配字符相同的那一位。可以簡單地理解為,我們將兩個已經遍歷過的模式串字符重合,導致我們可以不用一位一位地移動,而是根據相同的字符來實現快速移動。
模式串: abcab
文本串:abcacababcab
但有時不光只會有單個字符重復:
模式串:abcabc
文本串:abcabdababcabc
當我們發現在第六位失配時,我們可以將模式串的第一二位移動到第四五位,因為它們相同 qwqqwq .
模式串: abcabc
文本串:abcabdababcabc
那么現在已經很明了了, KMPKMP 的重頭戲就在於用失配數組來確定當某一位失配時,我們可以將前一位跳躍到之前匹配過的某一位。而此處有幾個先決條件需要理解:
1、我們的失配數組應當建立在模式串意義下,而不是文本串意義下。因為顯然模式串要更加靈活,在失配后換位時,更靈活簡便地處理。
2、如何確定位置呢?
首先我們要明白,基於先決條件 11 而言,我們在預處理時應當考慮當模式串的第 ii 位失配時,應當跳轉到哪里.因為在文本串中,之前匹配過的所有字符已經沒有用了——都是匹配完成或者已經失配的,所以我們的 kmpkmp 數組(即是用於確定失配后變化位置的數組,下同)應當記錄的是:
在模式串 str1str1 中,對於每一位 str1(i)str1(i) ,它的 kmpkmp 數組應當是記錄一個位置 jj , j \leq ij≤i 並且滿足 str1(i)=str1(j)str1(i)=str1(j) 並且在 j!=1j!=1 時理應滿足 str1(1)str1(1) 至 str1(j-1)str1(j−1) 分別與 str(i-j+1)str(i−j+1) ~ str1(i-1)str1(i−1) 按位相等
上述即為移位法則
3、從前綴后綴來解釋 KMPKMP :
首先解釋前后綴(因為太簡單就不解釋了 qwqqwq ):
給定串:ABCABA
前綴:A,AB,ABC,ABCA,ABCAB,ABCABA
后綴:A,BA,ABA,CABA,BCABA,ABCABA
其實剛才的移位法則就是對於模式串的每個前綴而言,用 kmpkmp 數組記錄到它為止的模式串前綴的真前綴和真后綴最大相同的位置(注意,這個地方沒有寫錯,是真的有嵌套 qwqqwq )。然而這個地方我們要考慮“模式串前綴的前綴和后綴最大相同的位置”原因在於,我們需要用到 kmpkmp 數組換位時,當且僅當未完全匹配。所以我們的操作只是針對模式串的前綴 --−− 畢竟是失配函數,失配之后只有可能是某個部分前綴需要“快速移動”。所以這就可以解釋 KMPKMP 中前后綴應用的一個特點:
KMPKMP 中前后綴不包括模式串本身,即只考慮真前綴和真后綴,因為模式串本身需要整體考慮,當且僅當匹配完整個串之后;而匹配完整個串不就完成匹配了嗎 qwqqwq
三、代碼實現
1、 kmp[i]kmp[i] 用於記錄當匹配到模式串的第 ii 位之后失配,該跳轉到模式串的哪個位置,那么對於模式串的第一位和第二位而言,只能回跳到 11 ,因為是 KMPKMP 是要將真前綴跳躍到與它相同的真后綴上去(通常也可以反着理解),所以當 i=0i=0 或者 i=1i=1 時,相同的真前綴只會是 str1(0)str1(0) 這一個字符,所以 kmp[0]=kmp[1]=1kmp[0]=kmp[1]=1 。
2、對於如何和文本串比對,很簡單:
int j; j=0;//j可以看做表示當前已經匹配完的模式串的最后一位的位置 //如果樓上看不懂,你也可以理解為j表示模式串匹配到第幾位了 for(int i=1;i<=la;i++) { while(j&&b[j+1]!=a[i])j=kmp[j]; //如果失配 ,那么就不斷向回跳,直到可以繼續匹配 if (b[j+1]==a[i]) j++; //如果匹配成功,那么對應的模式串位置++ if (j==lb) { cout<<i-lb+1<<endl; j=kmp[j]; //繼續匹配 } }
3、那么我們該如何處理 kmpkmp 數組呢?我們可以考慮用模式串自己匹配自己
j=0; for (int i=2;i<=lb;i++) { while(j&&b[i]!=b[j+1]) //此處判斷j是否為0的原因在於,如果回跳到第一個字符就不 用再回跳了 j=kmp[j]; //通過自己匹配自己來得出每一個點的kmp值 if(b[j+1]==b[i])j++; kmp[i]=j; //i+1失配后應該如何跳 }
那么這個“自己匹配自己”該如何理解呢?我們可以這么想: 首先,在單次循環只有一個 ifif 來判斷的原因在於每次至多向后多求一位的 nextnext ;
並且 jj 是擁有可繼承性的,由於 jj 是用於比對前綴后綴的,那么對於一組前后綴而言,第 i-1i−1 和第 j-1j−1 位之前均相同或者有不同,決定着 ii 和 jj 匹配的結果是從 00 開始還是基於上一個 jj 繼續 ++++
貼標程:
#include<iostream> #include<cstring> #define MAXN 1000010 using namespace std; int kmp[MAXN]; int la,lb,j; char a[MAXN],b[MAXN]; int main() { cin>>a+1; cin>>b+1; la=strlen(a+1); lb=strlen(b+1); for (int i=2;i<=lb;i++) { while(j&&b[i]!=b[j+1]) j=kmp[j]; if(b[j+1]==b[i])j++; kmp[i]=j; } j=0; for(int i=1;i<=la;i++) { while(j>0&&b[j+1]!=a[i]) j=kmp[j]; if (b[j+1]==a[i]) j++; if (j==lb) {cout<<i-lb+1<<endl;j=kmp[j];} } for (int i=1;i<=lb;i++) cout<<kmp[i]<<" "; return 0; }
那么時間復雜度為 \Theta(m+n)Θ(m+n) ,比朴素算法有了極大的優化。
Extra \ \ KnowledgeExtra Knowledge 淺析復雜度證明
題外話:本來想扯攤還分析來着,但是 rqyrqy 說的好像比較直接易懂,於是在這里就引用了 rqyrqy 的話:
每次位置指針 i++i++ 時,失配指針 jj 至多增加一次,所以 jj 至多增加 lenlen 次,從而至多減少 lenlen 次,所以就是 \Theta(len_N + len_M) = \Theta(N + M)Θ(lenN+lenM)=Θ(N+M) 的
總之很迷就對了(逃
其實我們也可以發現, KMPKMP 算法之所以快,不僅僅由於它的失配處理方案,更重要的是利用前綴后綴的特性,從不會反反復復地找,我們可以看到代碼里對於匹配只有一重循環,也就是說 KMPKMP 算法具有一種“最優歷史處理”的性質,而這種性質也是基於 KMPKMP 的核心思想的。
參考文章:
https://blog.csdn.net/gao506440410/article/details/81812163


