【演算法】KMP演算法

HinanawiTenshi發表於2021-02-13

簡介

KMP演算法由 Knuth-Morris-Pratt 三位科學家提出,可用於在一個 文字串 中尋找某 模式串 存在的位置。
本演算法可以有效降低在一個 文字串 中尋找某 模式串 過程的時間複雜度。(如果採取樸素的想法則複雜度是 \(O(MN)\)

題面:https://www.luogu.com.cn/problem/P3375

這裡樸素的想法指的是列舉 文字串 的起點,然後讓 模式串 從第一位開始一個個地檢查是否配對,如果不配對則繼續列舉起點。

前置知識

真字首
指字串左部的任意子串(不包含自身),如 abcde 中的 a,ab,abc,abcd 都是真字首但 abcde 不是。

真字尾
指字串右部的任意子串(不包含自身),如 abcde 中的 e,de,cde,bcde 都是真字尾但 abcde 不是。

字首函式
一個字串中最長的、相等的真字首與真字尾的長度, 如AABBAAA對應的字首函式值是 \(2\)

原理

注意:在分析的時候,我們規定字串的下標從 \(1\) 開始。

開始:
我們記掃描模式串的指標為j,而掃描文字串的指標為i,假設一開始i,j都在起點,然後讓它們一直下去直到完全匹配或者失配,比如:

j
ABCD

i
ABCDEFG

然後

 j
ABCD

 i
ABCDEFG

最後在此完成了一次匹配,類似地如果ABCD改為ABCC則在此失配。

   j
ABCD

   i
ABCDEFG

i,j運作模式如上。



KMP演算法就是,當模式串和文字串失配的時候,j指標從真字尾的末尾跳到真字首的末尾,然後從真字首後一位開始繼續匹配。(從而起到減少配對次數,這便是KMP演算法的核心原理)

結合例子解釋:

模式串: \(AABBAAA\)

文字串: \(AABBAABBAAA\)

j指標在最後一個A處失配。

      j
AABBAAA
      i
AABBAABBAAA

因為此時 以j為尾的字首 所對應的字首函式值是 \(2\) ,所以 j指標 跳到這裡:

 j
AABBAAA
      i
AABBAABBAAA

然後從下一位開始繼續配對:

  j
AABBAAA
      i
AABBAABBAAA

最後

      j
AABBAAA
          i
AABBAABBAAA

可以看出,KMP能夠有效減少配對次數。

實現

我們記模式串p文字串s

從上面的模擬中,我們發現需要預處理出一個陣列(記之為next[]),它儲存模式串中字首對應的字首函式\(\pi()\),如對於字串ABCABC

\(\pi(0)=0\) (因為什麼都沒有)
\(\pi(1)=0\)A甚至沒有真字首真字尾
\(\pi(2)=0\)AB
\(\pi(3)=0\)ABC
\(\pi(4)=1\)ABCA
\(\pi(5)=2\)ABCAB
\(\pi(6)=3\)ABCABC

同樣地,我們發現如果用暴力樸素的想法來統計複雜度是 \(O(N^2)\) 不好,於是採用類似於上面的方法,只不過模式串配對的物件是自己罷了。

可以結合程式碼理解,並注意舉例,嘗試在紙上模擬這個過程。

for(int i=2,j=0;i<=lenp;i++){
        while(j && p[j+1]!=p[i]) j=next_[j]; // 如果j指向元素的下一個元素會和當前配對位置失配,則j跳回去
        if(p[j+1]==p[i]) j++; //如果能夠配對上,j++
        next_[i]=j; //記錄當前位置的字首函式π
}

完整程式碼:

#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5;
char p[N],s[N];
int next_[N];

int main(){
    cin>>s+1>>p+1;

    int lenp=strlen(p+1),lens=strlen(s+1);
    // build next array
    for(int i=2,j=0;i<=lenp;i++){
        while(j && p[j+1]!=p[i]) j=next_[j]; // 如果j指向元素的下一個元素會和當前配對位置失配,則j跳回去
        if(p[j+1]==p[i]) j++; //如果能夠配對上,j++
        next_[i]=j; //記錄當前位置的字首函式π
    }

    for(int i=1,j=0;i<=lens;i++){
        while(j && p[j+1]!=s[i]) j=next_[j];
        if(p[j+1]==s[i]) j++;

        // if match
        if(j==lenp){
            j=next_[j];
            cout<<i-lenp+1<<endl;
        }
    }

    for(int i=1;i<=lenp;i++) cout<<next_[i]<<' ';
    cout<<endl;

    return 0;
}

複雜度

\(O(N+M)\)

例題

https://www.acwing.com/problem/content/1054/

分析

結合了狀態機,KMP的一道DP題。
遞推方程:f[i][u]+=f[i-1][j] 其中j表示一個狀態,u表示由j轉移而來的狀態,i表示填到文字串的第幾位了。

程式碼
#include<bits/stdc++.h>
using namespace std;

const int N=55, mod=1e9+7;

int nxt[N],f[N][N];

int n;
char p[N];

int main(){
    cin>>n>>p+1;
    
    int lenp=strlen(p+1);
    for(int j=0,i=2;i<=lenp;i++){
        while(j && p[j+1]!=p[i]) j=nxt[j];
        if(p[j+1]==p[i]) j++;
        nxt[i]=j;
    }
    
    f[0][0]=1;
    for(int i=1;i<=n;i++)
        for(int j=0;j<lenp;j++)
            for(char k='a';k<='z';k++){
                int u=j;
                while(u && p[u+1]!=k) u=nxt[u];
                if(p[u+1]==k) u++;
                
                if(u<lenp) f[i][u]=(f[i-1][j]+f[i][u])%mod;
            }
    
    int res=0;
    for(int i=0;i<lenp;i++) res=(f[n][i]+res)%mod;
    
    cout<<res<<endl;
    
    return 0;
}

相關文章