1. 程式人生 > >字串匹配 & KMP演算法

字串匹配 & KMP演算法

初識KMP

  • 期末的時候學習了KMP演算法,雖然一開始的確聽得是一頭霧水,但是到現在,已經基本懂得了其中的原理,於是在這裡把自己的理解寫出來,再配上自己做的圖示,希望對大家的學習有幫助,要是有什麼疑問或建議,歡迎留言評論。^-^
  • KMP是一種用於字串匹配的演算法,至於為什麼叫KMP呢,那是因為它是由D.E.Knuth和J.H.Morris和V.R.Pratt同時發現的,所以人們稱它為K-M-P演算法。
  • 所謂字串匹配操作,可以這樣理解:
    • 有一個長度為m 的字串S,我們可以稱它為目標串
    • 有一個長度為 n 的字串T,我們可以稱它為模式串
    • 字串匹配操作所要做的,就是要你在S中找出所有與T相同的子串

方法講解

下面就來講一講KMP其中的思路 (為程式方便,這裡 S 和 T 兩個字串下標都是從0開始的) (因為對於不同的題目,對結果要求可能不同,所以程式中處理答案的部分就直接用“`ans++“`來代替)

暴力方法

講KMP之前,我覺得應該先說一說最暴力的方法,也就是說直接逐位比較,先比較S[0..n-1]和T[0..n-1]是否一樣,然後比較S[1..n]和T[0..n-1]是否一樣,依此類推
void baoli(){
    for (int i=0; i+n<=m; i++){
        int j;
        for
(j=0; j<n && S[i+j]==T[j]; j++); if (j>=n) ans++; //j>n代表整個T串都匹配成功了 } }

暴力演示

思考

資料小還沒什麼,可一旦資料變大,這種暴力方法就變得很慢了,可以看出,這種暴力在最倒黴的狀況下(比如 S 和 T 都是一長串相同的字母),時間幾乎可以到達O(n*m),這時,就會想到優化一下這個方法,於是人們就發明出了KMP。

KMP方法

  • 從前面的暴力方法中可以發現,時間大多是浪費在很多重複的不合法位置上,也就是說,比如在S中匹配到T的第i位發現對不上(失配)了,若用暴力方法,則只是單純地往後移一位,要是可以預處理出這時應給往後移動幾位,那麼速度就會快很多。

next陣列

  • 那麼,我們就可以先對T預處理出一個next陣列,表示滿足T[0..x+1]=T[(i-x)..i]的最大的x
  • 可以想象圖中黑長條是T,而兩段小藍條是相等的,且對於當前的i沒有更長的這樣的藍條,那麼圖中藍色箭頭指向的就是next[i]了
    這裡寫圖片描述
  • 要怎麼求這個next陣列呢?可以這樣:
    • 初始一下next[0]=-1
    • 列舉 i 從1到n,依次求next[i]
    • 再用一個變數 k 記錄答案,初始值為next[i-1],即圖中左邊那個籃框的位置,不過此時 k+1 這裡不確定是不是i那個字元,再判斷一下,若T[k+1]與T[i]不同,則k=next[k],直到k小於零為止。
void get_next(){
    next[0]=-1;
    for (int i=1; i<n; i++){
        int k=next[i-1];
        while (k>-1 && T[k+1]!=T[i]) k=next[k];
        if (T[k+1]==T[i]) k++;
        next[i]=k;
    }
}
  • 附上幾組求next陣列的演示動畫,可以截圖下來慢慢看
  • ababa
  • ababa
  • ababacabc
  • ababacabc

kmp

接下來就簡單了,看一看動畫,再看一看程式應該就可以差不多懂了,之前手畫了一個示意圖,T陣列當時用的是P,看看畫得清不清楚~
這裡寫圖片描述

void kmp(){
    int k=0;                //k代表T字串中T中k以及之前的都匹配好了(T[0..k]=S[i-k-1..i-1])
    for (int i=0; i<m; i++){        //i代表S字串當前匹配位是第i位
        while (k>-1 && S[i]!=T[k+1]) k=next[k];     //可以看做T一直向右移知道匹配成功或不能移了
        if (S[i]==T[k+1]) k++;  //若T[k+1]位與S[i],說明第k+1位也是匹配成功的,k就再加一下
        if (k==n-1) k=next[k], ans++;   //要是k是T的最後一位,說明T整串都匹配成功,處理一下答案
    }
}

第三行代表著下一個T應跳的位置
S: ababcabcababcababcac
T: abcababc
這裡寫圖片描述

S: ababababcababdababadb
T: ababa
這裡寫圖片描述

結束

可以看到,用了kmp之後,時間複雜度差不多就是O(m)了很快吧!
學完之後要是還不太懂,可以多打幾遍,做些題,打多了就熟了,下面附上洛谷3375-kmp模板題的程式(這題題面上n和m是和程式裡是反過來的,不要介意)

#include <cstdio>
#include <cstring>
using namespace std;
char S[1000010],T[1000010];
int m,n,next[1000010];

void get_next(){
    next[0]=-1;
    for (int i=1; i<n; i++){
        int k=next[i-1];
        while (k>-1 && T[k+1]!=T[i]) k=next[k];
        if (T[k+1]==T[i]) k++;
        next[i]=k;
    }
}

void kmp(){
    int k=0;
    for (int i=0; i<m; i++){
        while (k>-1 && S[i]!=T[k+1]) k=next[k];
        if (S[i]==T[k+1]) k++;
        if (k==n-1) k=next[k],printf("%d\n",i-n+2);
    }
}

int main(){
    scanf("%s%s",S,T); m=strlen(S); n=strlen(T);
    get_next();
    kmp();
    for (int i=0; i<n; i++) printf("%d ",next[i]+1);
}