淺談KMP算法

不想寫題。不如寫寫算法總結?ios

KMP

前(che)言(dan)

之前都不知道 \(KMP\) 爲何叫 \(KMP\) ,如今才明白:該算法是三位大牛:D.E.KnuthJ.H.MorrisV.R.Pratt同時發現的,以其名字首字母命名。
\(KMP\) 能夠在\(O(n+m)\)的時間複雜度內解決斷定一個字符串\(A[1\)~ \(N]\)是否爲字符串\(B[1\)~\(M]\)的字串的問題。
雖然Hash好像也能夠線性解決這個問題算法

我會暴力

固然一個 \(O(nm)\) 的作法是很是顯然的:直接枚舉A串在B串的開始位置而後日後一位一位的比較。
考慮這樣的作法有什麼能夠優化的地方?
考慮以下場景,某次匹配中按 \(O(nm)\) 的方法進行到這一步。
圖中下方是串 \(A\) ,上方是串 \(B\) ,咱們已經匹配到最後一個字符,匹配就快成功,但不幸的是最後一位出錯了,咱們又要從頭匹配。
可是

咱們發現綠框框住的部分匹配,但咱們以後還要從新匹配浪費了時間。咱們能不能記錄一些信息,而後下次直接從綠框後面的位置開始匹配,這樣不就節約了時間。
還有,能不能選出一些可能完成匹配的位置進行匹配,這樣就不用從每個位置匹配整個串,也節約了時間。
\(KMP\) 算法就這樣誕生了數組

算法流程

KMP算法定義了一個next數組
其中 \(next[i]\) 表示A中以i結尾的非前綴子串A的前綴能匹配的最長長度。
那麼這個東西有什麼用啊?感受好玄妙,爲何非要是非前綴?
別急咱們先來講說\(next\)數組的求法。
我會 \(O(n^2)\) 的求法。。。
其實能夠 \(O(n)\) 求。

假設咱們已經求出了next 1~next i,圖中綠框框起來的就是能匹配的最長的A中以i結尾的非前綴子串A的前綴
咱們如今要求 \(next[i+1]\)

(這裏的j就至關於 \(next[i]\) )
顯然當兩個紅圈圈起的位置上的字符相等,那麼 \(next[i+1]=j+1\)
那麼不相等怎麼辦?
從新匹配嗎,那不就 \(O(n^2)\) 了嗎?
我不會了媽媽救我

咱們先設出 \(next[j]\) 的位置。顯然兩個藍框框起的串匹配

由於綠框框起的串匹配,是否是四塊藍框框起的串互相匹配。

四塊都互相匹配了顯然這兩塊是匹配的。
咦,等等,好像有點眼熟!
這跟開始的一張圖片很像。
也許你已經猜到接下來該作什麼了。
咱們看看 \(next[j]+1\)\(i+1\) 位置上的字符是否相等。若是相等 \(next[i+1]=next[j]+1\) ,若是還不相等咱們把 \(next[j]\) 當作 \(j\) ,繼續取 \(next[j]\) 。。。。。。(一直這樣跳\(next\)跳下去)
知道最後找到一個 \(j\) ,使得 \(j+1\) 位置上的字符跟 \(i+1\) 位置上的字符相等。或找不到字符跟 \(i+1\) 位置上的字符相等 \(next[i+1]\) 就是 \(0\)優化

正確性

這樣的正確性有保證嗎?
咱們想一直跳 \(next\) 實際上就是在遍歷(全部能匹配的A中以i結尾的非前綴子串A的前綴的長度)(名詞太長用括號括起來)。由於 \(next\) 記錄的是最長長度,因此能夠不重不漏遍歷全部狀況(一直取小於這個數中最大的就能夠遍歷全部狀況)。spa

或者這樣想

若是漏掉了紫色框的狀況。即假設紫色框框起的部分是(長度比綠框小的且是最長的能匹配的最長的A中以j結尾的非前綴子串A的前綴)

那麼由於綠框框起的串匹配,四個紫框框起的串互相匹配。
而後 \(next[j]\) 就不在圖中所在位置了,也就是說與 \(next[j]\) 表明A中以j結尾的非前綴子串A的前綴能匹配的最長長度矛盾
因此用上面說的方法正確性是對的。code

複雜度

那這樣感受複雜度又成 \(O(n^2)\) 的了
實際上是 \(O(n)\) 的,咱們來分析一波
這是求 \(next\) 數組的代碼blog

for(int i=2,j=0;i<=len;i++){
    while(j&&A[j+1]!=A[i])j=nxt[j];
    if(A[j+1]==A[i])j++;
    nxt[i]=j;
}

每次求 \(next[i]\) 時咱們程序裏的記錄 \(next[i]\) 的變量 \(j\) 最多+1,一共最多加 \(n\) 次。而後每次跳 \(next\) ,j只會減少。
因此最多跳 \(n\)\(next\) 。複雜度\(O(n)\)
至此咱們終於把如何求 \(next\) 數組講完了。圖片

其實後面的就簡單了。
咱們回到最初的起點。。
咱們依然是按位匹配兩個串。當不匹配的時候。咱們把 \(i\) 改爲 \(next[i]\) 繼續匹配就好。
就像這樣。字符串

板子題

P3375 【模板】KMP字符串匹配get

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=1010000;
char s1[N],s2[N];
int len1,len2,nxt[N];
int main(){
    scanf("%s",s1+1);
    scanf("%s",s2+1);
    len1=strlen(s1+1);
    len2=strlen(s2+1);
    for(int i=2,j=0;i<=len2;i++){
        while(j&&s2[j+1]!=s2[i])j=nxt[j];
        if(s2[j+1]==s2[i])j++;
        nxt[i]=j;
    }
    for(int i=1,j=0;i<=len1;i++){
        while((j&&s2[j+1]!=s1[i])||j==len2)j=nxt[j];
        if(s2[j+1]==s1[i])j++;
        if(j==len2)printf("%d\n",i-j+1);
    }
    for(int i=1;i<=len2;i++)printf("%d ",nxt[i]);
    return 0;
}

擴展

\(KMP\) 除了單模式串匹配以外還有什麼用處呢?
它還能夠求循環節
有一個結論是若是\((len\)%\((len-next[len])==0)\)那麼最小循環節長度\(len-next[len]\)
那麼最小的循環節出現次數就是\(len/(len-next[len])\)
爲何呢?
爲了方便解釋,把字符串複製成兩個。兩個綠框分別表明能匹配的最長的A的非前綴後綴A的前綴。兩個綠框框起的串根據\(next\)數組的定義相等。
由於兩個同樣的串,圖中紅圈圈起的串顯然相等。
而後由於兩個綠框框起的串匹配,三個紅圈圈起的串顯然匹配。
又由於兩個串是同樣的,因此這四個紅圈圈起的串互相匹配。
進而得出這些紅圈表明的串都相等。
發現紅圈圈起的串是循環節,由於\(next\)數組表明的是最大值,因此這個循環節是最小的。
因此若是\((len\)%\((len-next[len])==0)\)那麼最小循環節長度\(len-next[len]\)
那麼最小的循環節出現次數就是\(len/(len-next[len])\)
注意必需要知足\((len\)%\((len-next[len])==0)\)
KMP求最小循環節的題POJ 2406 Power Strings
本文只是講解算法,真正掌握它還須要多刷題。
完結撒花

相關文章
相關標籤/搜索