1. 程式人生 > 實用技巧 >對KMP演算法的理解

對KMP演算法的理解

KMP演算法

對於KMP的講解,我認為如何更好地理解和掌握 KMP 演算法? - 阮行止的回答 - 知乎這篇回答的講解已經非常細緻了,沒有必要對這種教程反覆造輪子。這篇回答中只有對於next快速構建的一些部分不是很清晰。因此,下面一方面以筆記的性質記錄我的理解,另一方面重述關於next構建的部分。

KMP的直覺過程

借用上面教程中的圖進行一下說明。下面的圖描述的是:從主串S中尋找模式串P的匹配過程。

KMP整個過程的核心是利用已有的資訊,對失配後的模式串進行“修正”(這裡非常像Manacher演算法)。這裡的“修正”是個很抽象的詞。就像上面的圖中,模式串的第一次匹配到最後一個字母‘d’時,發現了錯誤,即當前不能再匹配下去了,因此需要向後移位一格,再匹配。第二次匹配時第一個字母就錯了,再移位一格。第三次第一個字母也不對,再移位一格。終於到了第四次,第一個字母能進行下去了,幸運的是第二個也能繼續下去,之後就可以繼續匹配第三個了。

這個過程可以發現一個特點,即對於主串來說,當前所指向的字元(即index_s)是不會往回走的,即一直是向後移動的。失配後需要重新調整的只有指向模式串的index_p。這在對KMP直覺的理解上是很重要的一點。

那麼剛才提到的“修正”指的是什麼?其實就是從某一次匹配出錯,利用某些資訊直接跳過“連第一個字母都匹配不上”的狀態,進入一定可以繼續匹配的過程。這個說法很不嚴謹,但是可以在一定程度上理解演算法在做什麼。

對於上面這個過程,之前提到的教程的描述如下:

  有些趟字串比較是有可能會成功的;有些則毫無可能。我們剛剛提到過,優化 Brute-Force 的路線是“儘量減少比較的趟數”,而如果我們跳過那些絕不可能成功的

字串比較,則可以希望複雜度降低到能接受的範圍。

這個過程究竟利用了什麼資訊,我覺得可以按下面的思路捋一下:

  1. 模式串本身是蘊含一定資訊的,比如第一個字母和第二個字母如果不一樣,那麼後移一位是一定沒有意義的。
  2. 如果當前S和P剛剛失配,意味著在這個字元前面的所有字元是“匹配的”
  3. 如果存在2中的匹配,那麼就把模式串本身的資訊傳遞給了主串S

到這裡,意思就很明確了。我們構建一個next,然後借用next中對前後綴的匹配關係,快速定位失配後的下一個index_p應該指向哪裡。

就像下圖:我們在匹配模式串的6和主串的6時出現了錯誤,那麼這意味著主串和模式串的[0-5]是相同的。根據模式串自身攜帶的資訊,我們知道模式串的[0-1]和[4-5]是相同的。根據前面匹配的特性,我們知道主串中的[0-1]和[4-5]也是相同的。因此下一次匹配時,我們可以讓模式串的[0-1]和主串的[4-5]對齊,直接從主串的index_s = 6和模式串的index_p = 2開始比較,就可以繼續了。值得注意的是,在從上次匹配到下次匹配的過程中,指向主串的index_s始終是沒有變的,需要修正的,只有模式串的index_p。

前面所有過程都是出於從直覺上理解KMP在做什麼,而其中的思路,為什麼這麼做,next資訊是什麼,為什麼需要next資訊,在前面大佬的回答如何更好地理解和掌握 KMP 演算法? - 阮行止的回答 - 知乎中講的非常清楚。

next的快速構建

這裡還是借用大佬的例子(我覺得他舉的例子很好)

當我們的p[x]和p[now]出現不匹配時,這裡next[x]不能繼續增加。因此需要把now回撥才能繼續匹配。

這裡最重要的一點是,雖然p[x]和p[now]失配了,但是這意味著子串A和子串B是完全匹配的

我們要調整now來重新尋找對應。而調整now實際上就是要找子串A的字首和子串B的字尾到底能重合多少(最大重合數量)。因為子串A和子串B是完全匹配的,因此我們要找的就是子串A的字首和子串A的字尾最大重合多少(相當於把子串B的字尾變成子串A的字尾,因為他倆完全匹配),而這個問題的答案,就是next[now - 1]。

如果明白了這個過程,再去看前面大佬的題解,那就很明晰了。

KMP的C++實現

#include <iostream>
#include <string>
#include <vector>
using namespace std;

// KMP類: 實現了KMP的主要功能:
// - 構建next串
// - 匹配
class KMP {
  string s;
  string pattern;

 public:
  KMP(const string &s, const string &pattern) : s(s), pattern(pattern) {}

  // 構建next資訊
  vector<int> buildNext(const string &s) {
    int n = s.size();
    vector<int> next(n, 0);
    int index = 1;
    int now = 0;

    while (index < n) {
      if (s[index] == s[now]) {
        next[index] = now;
        ++index;
        ++now;
      } else if (now != 0) {
        now = next[now - 1];
      } else {
        next[index] = 0;
        ++index;
      }
    }
    return next;
  }

  // 尋找與匹配
  bool find() {
    auto next = buildNext(pattern);
    int n = s.size();
    int index_s = 0;
    int index_p = 0;

    // 輸出當前任務的資訊:
    printInfo(next);

    while (index_s < n) {
      if (s[index_s] == pattern[index_p]) {
        ++index_p;
        ++index_s;
      } else if (index_p != 0) {
        index_p = next[index_p - 1];
      } else {
        ++index_s;
      }

      if (index_p == pattern.size()) {
        cout << "s[" << index_s - pattern.size() << ", " << index_s - 1
             << "] = pattern" << endl;
        return true;
      }
    }
    return false;
  }

  void printInfo(const vector<int> &next) {
    cout << "s = " << s << endl;
    cout << "    ";
    for (int i = 0; i < s.size(); ++i) {
      cout << i % 10;
    }
    cout << endl;

    cout << "pattern = " << pattern << endl;

    cout << "next = ";
    for (auto &x : next) {
      cout << x;
    }
    cout << endl;
  }
};

int main() {
  // 簡單驗證
  string s = "abcddddabcddabxcddddabxcddddxabx";
  string pattern = "abxcddddxabx";
  KMP k(s, pattern);
  cout << k.find() << endl;
  cout << endl;

  string mode2 = "abxcddddxabxp";
  KMP k2(s, mode2);
  cout << k2.find() << endl;
  cout << endl;

  string s3 = "ababaabaabac";
  string mode3 = "abaabac";
  KMP k3(s3, mode3);
  cout << k3.find() << endl;

  return 0;
}