字串演算法--$\mathcal{KMP,Trie}$樹

MathicTeaixon發表於2023-03-29

\(\mathcal{KMP演算法}\)

實際上,完全沒必要從\(S\)的每一個字元開始,暴力窮舉每一種情況,\(Knuth、Morris\)\(Pratt\)對該演算法進行了改進,稱為KMP演算法。

\(KMP\)的精髓在於,對於每次失配之後,我都不會從頭重新開始列舉,而是根據我已經得知的資料,從某個特定的位置開始匹配;而對於模式串的每一位,都有唯一的“特定變化位置”,這個在失配之後的特定變化位置可以幫助我們利用已有的資料不用從頭匹配,從而節約時間。

特點:1. \(i\) 不回退 2. \(j\) 回退的位置有講究 3.構建一個輔助陣列( \(nxt\) 陣列)來跳過不必要的字元比較,從而提高搜尋速度。

\(\mathcal{實現流程}\)

image

image

為了清楚地表述目的, \(T\)\(S\) 失配前的部分作為 \(T'\) 來表述,此時尋找下一個開始匹配的標誌頭。而找到下一個標誌頭的方式為:

找到 \(T'\) 的最長相同字首與字尾

image

\(\color{red}{這樣找所有的字首和字尾比較,是不是也是暴力窮舉??那該怎麼辦呢??}\)

\(\color{red}{ans:當然是要用到動態規劃遞推啦。}\)

\(\mathcal{構建 Nxt 陣列}\)

\(nxt\) 陣列用於表示當前字元匹配失敗時,模式串應該回退到哪個位置。對於模式串 \(p\) ,我們遍歷其每個字元,並用一個指標 \(j\) 表示已匹配的字元數。當模式串中的兩個字元匹配時,我們更新指標 \(j\) 的值,否則,我們回退 \(j\)\(nxt[j]\) 的位置。透過這種方式,我們可以為模式串構建一個 \(nxt\) 陣列,其中 \(nxt[i]\) 表示當模式串中第 \(i\) 個字元匹配失敗時,應該回退到的位置。

\(\mathcal{實際字元匹配過程}\)

我們使用兩個指標 \(i\)\(j\) 分別遍歷原字串 \(s\) 和模式串 \(p\) 。如果當前字元匹配,則同時移動 \(i\)\(j\) 。如果字元不匹配,我們根據 \(nxt\) 陣列回退 \(j\) 的位置,直到找到匹配的字元或回退到模式串的開頭。當 \(j\) 等於模式串長度 \(m\) 時,表示找到了一個匹配,輸出匹配位置,並將 \(j\) 重置為 \(0\)

\(\mathcal{模板程式碼實現}\)

#include <iostream>
using namespace std;
const int N = 1e+6 + 10;
int nxt[N], n, m;
char p[N], s[N];
int main()
{
    cin >> n >> s + 1 >> m >> p + 1;
    // build next arraylist
    for (int i = 2, j = 0; i <= m; i++)
    {
        while (j && p[i] != p[j + 1]) j = nxt[j];
        if (p[i] == p[j + 1]) j++;
        nxt[i] = j;
    }
    // marry the str
    for (int i  = 1, j = 0; i <= n; i++)
    {
        while (j && s[i] != p[j + 1]) j = nxt[j];
        if (s[i] == p[j + 1]) j++;
        if (j == m) {
            cout << i - m << " ";
            j = 0;
        }
    }
    cout << endl;
    return 0;
}

\(\mathcal{Trie\,樹}\)

字典樹是一種高效的字串資料結構,尤其適用於處理大量字串的時候,它透過將字串的公共字首合並在一起,節省空間並提高查詢速度。

\(\mathcal{實現流程}\)

\(\mathcal{初始化變數和資料結構}\)

定義一個字典樹結構( \(tree\) 陣列)和一個記錄字串出現次數的陣列( \(vis\) 陣列)。同時定義一個計數器 \(flag\) 用於記錄字典樹中節點的數量。二維陣列 \(tree\) 表示字典樹的結構,其中 \(tree[i][j]\) 表示第 \(i\) 個節點的第 \(j\) 個子節點。

\(\mathcal{子功能實現}\)

\(\mathcal{insert}\)

實現一個 \(insert\) 函式,用於向字典樹中插入一個字串。它遍歷字串中的每個字元,將字元轉換為陣列下標(透過減去' \(a\) '並加上 \(1\) )。如果當前字元對應的子節點不存在,則建立一個新的節點並更新節點計數器。最後,在字串末尾的節點中,更新字串出現的次數。

\(\mathcal{query}\)

實現一個 \(query\) 函式,用於查詢字典樹中字串的出現次數。它遍歷字串中的每個字元,將字元轉換為陣列下標。如果當前字元對應的子節點不存在,說明字串不存在,查詢結束。否則,將指標移動到子節點。最後,返回字串末尾節點對應的出現次數。

\(\mathcal{主程式邏輯}\)

讀取運算元量 \(n\) ,然後迴圈處理每個操作。對於每個操作,讀取操作型別( \(ope\) )和操作字串( \(str\) )。如果操作型別為 "\(i\)" ,呼叫 \(insert\) 函式插入字串;如果操作型別為其他(例如查詢操作),呼叫 \(query\) 函式查詢字串,並輸出查詢結果。

\(\mathcal{模板程式碼實現}\)

#include <iostream>
using namespace std;
const int N = 1e+6 + 10;
int n, flag = 1;
string ope, str;
int tree[N][27], vis[N][27];
void insert(string str)
{
    int pos = 0;
    int tmp = 0;
    for (int i = 0; i < str.size(); i++)
    {
        tmp = str[i] - 'a' + 1;
        if (tree[pos][tmp] == 0) tree[pos][tmp] = flag ++;
        pos = tree[pos][tmp];
    }
    vis[pos][tmp] ++;
}
int query(string str)
{
    int pos = 0;
    int tmp = 0;
    for (int i  = 0; i < str.size(); i++)
    {
        tmp = str[i] - 'a' + 1;
        if (tree[pos][tmp] == 0) break;
        pos = tree[pos][tmp];
    }
    return vis[pos][tmp];
}
int main()
{
    cin >> n;
    while (n--)
    {
        cin >> ope >> str;
        if (ope == "i") insert(str);
        else cout << query(str) << endl;
    }
    return 0;
}

相關文章