AC 自動機 多模式串匹配

2021-09-13 01:31:25 字數 4128 閱讀 5576

**上的敏感詞過濾是怎麼實現的呢?
實際上,這些功能最基本的原理就是字串匹配演算法,也就是通過維護乙個敏感詞的字典,當使用者輸入一段文字內容後,通過字串匹配演算法來檢查使用者輸入的內容是否包含敏感詞。

bf、rk、bm、kmp 演算法都是針對只有乙個模式串的字串匹配演算法,而要實現乙個高效能的敏感詞過濾系統,就需要用到多模式匹配演算法了。

多模式匹配演算法,就是在多個模式串和乙個主串之間做匹配,也就是在乙個主串中查詢多個模式串。

敏感詞過濾,也可以通過單模式匹配演算法來實現,那就是針對每個敏感值都做一遍單模式匹配。但如果敏感詞很多,並且主串很長,那我們就需要遍歷很多次主串,顯然這種方法是非常低效的。

而多模式匹配演算法只需要掃瞄一遍主串,就可以一次性查詢多個模式串是否存在,匹配效率就大大提高了。那如何基於 trie 樹實現敏感詞過濾功能呢?

我們可以首先對敏感詞字典進行預處理,構建成 trie 樹。這個預處理的操作只需要做一次,如果敏感詞字典動態更新了,我們只需要在 trie 樹中新增或刪除乙個字串即可。

使用者輸入乙個文字內容後,我們把使用者輸入的內容作為主串,從第乙個字元開始在 trie 樹中進行匹配。當匹配到葉子節點或者中途遇到不匹配字元的時候,我們就將主串的匹配位置後移一位,重新進行匹配。

基於 trie 樹的這種處理方法,有點類似單模式匹配的 bf 演算法。我們知道 kmp 演算法在 bf 演算法基礎上進行了改進,每次匹配失敗時,盡可能地將模式串往後多滑動幾位。同樣,在這裡,我們是否也可以對多模式串 trie 樹進行同樣的改進呢?這就要用到 ac 自動機演算法了。

ac 自動機演算法,全稱是 aho-corasick 演算法。ac 自動機實際上就是在 trie 樹之上,加了類似於 kmp 演算法的 next 陣列,只不過此處的陣列是構建在樹上罷了

class acnode

};

ac 自動機的構建包含兩個操作:

trie 樹中的每乙個節點都有乙個失敗指標,它的作用和構建過程,和 kmp 演算法中 next 陣列極其相似。

假設我們沿著 trie 樹走到 p 節點,也就是下圖中的紫色節點,那 p 的失敗指標也就是從根節點走到當前節點所形成的字串 abc,和所有模式串字首匹配的最長可匹配字尾子串,這裡就是 bc 模式串。

字串 abc 的字尾子串有 c 和 bc,我們拿它們和其它模式串進行匹配,如果能夠匹配上,那這個字尾就叫作可匹配字尾子串。在乙個字串的所有可匹配字尾子串中,長度最長的那個叫作最長可匹配字尾子串。我們就將乙個節點的失敗指標指向其最長可匹配字尾子串對應的模式串字首的最後乙個節點。

其實,如果我們把樹中相同深度的節點放到同一層,那麼某個節點的失敗指標只有可能出現在它所在層的上面。因此,我們可以像 kmp 演算法那樣,利用已經求得的、深度更小的那些節點的失敗指標來推導出下面節點的失敗指標。

首先,根節點的失敗指標指向 null,第一層節點的失敗指標都指向根節點。然後,繼續往下遍歷,如果 p 節點的失敗指標指向 q,那麼我們需要看節點 p 的子節點 pc 對應的字元,是否也可以在節點 q 的子節點 qc 中找到。如果找到了乙個子節點 qc 和 pc 的字元相同,則將 pc 的失敗指標指向 qc。

如果找不到乙個子節點 qc 和 pc 的字元相同,那麼我們繼續令 q = q->fail,重複上面的查詢過程,直到 q 為根節點為止。如果還沒有找到,那就將 pc 的失敗指標指向根節點。

// 構建失敗指標

void build_failure_pointer()

q = q->fail;

}if (q == null) pc->fail = root;

}ac_queue.push(pc);}}

}

通過按層來計算每個節點的子節點的失敗指標,例中最後構建完之後的 ac 自動機就是下面這個樣子。

接下來,我們看如何在 ac 自動機上匹配子串?首先,主串從 i=0 開始,ac 自動機從指標 p=root 開始,假設模式串是 b,主串是 a。

// 在 ac 自動機中匹配字串

void match_string(const char str)

p = p->children[index];

if (p == null) p = root; // 沒有可匹配的,從根節點開始重新匹配

acnode *temp = p;

while (temp != root)

temp = temp->fail;}}

}

全部**如下:

#include #include #include using namespace std;

class acnode

};class ac

// 向 trie 樹中新增乙個字串

void insert_string(const char str)

cur = cur->children[index];

}cur->is_ending_char = true;

cur->length = strlen(str);

}// 構建失敗指標

void build_failure_pointer()

q = q->fail;

}if (q == null) pc->fail = root;

}ac_queue.push(pc);}}

}// 在 ac 自動機中匹配字串

void match_string(const char str)

p = p->children[index];

if (p == null) p = root; // 沒有可匹配的,從根節點開始重新匹配

acnode *temp = p;

while (temp != root)

temp = temp->fail;}}

}};int main()

; char str[5] = ;

ac test;

for (int i = 0; i < 7; i++)

test.build_failure_pointer();

"however, what about her boyfriend?");

test.match_string("abcfabce");

return 0;

}

首先,構建 trie 樹的時間複雜度為 o(m*len),其中 len 表示敏感詞的平均長度,m 表示敏感詞的個數。

其次,假設 trie 樹中總共有 k 個節點,每個節點在構建失敗指標的時候,最耗時的就是 while 迴圈部分,這裡 q = q->fail,每次節點的深度都在減小,樹的最大深度為 len,因此每個節點構建失敗指標的時間複雜度為 o(len),整個失敗指標構建過程的時間複雜度為 o(k*len)。不過,ac 自動機的構建過程都是預先處理好的,構建好之後並不會頻繁更新。

最後,假設主串的長度為 n,匹配的時候每乙個 for 迴圈裡面的時間複雜度也為 o(len),總的匹配時間複雜度就為 o(n*len)。因為敏感詞不會很長,而且這個時間複雜度只是乙個非常寬泛的上限,實際情況下,可能近似於 o(n),所以,ac 自動機匹配的效率非常高。

從時間複雜度上看,ac 自動機匹配的效率和 trie 樹一樣,但是一般情況下,大部分節點的失敗指標都指向根節點,ac 自動機實際匹配的效率要遠高於 o(n*len)。只有在極端情況下,ac 自動機的效能才會退化為和 trie 樹一樣。

參考資料-極客時間專欄《資料結構與演算法之美》

AC自動機(多模式串匹配)

虛線部分代表fail指標 ac自動機模板題鏈結 靜態陣列版本 推薦 量少易寫,更快易寫 include using namespace std class aho corasick 儲存結構 trie樹 void insert const char str cnt p void build fail...

AC自動機(多模式匹配)

ac自動機主要解決的問題 多模式匹配 kmp則屬於單模式匹配 n個單詞在m個字元的文章中,出現過多少次。主要分三步 構建trie樹 構建失敗指標 尋找匹配個數 trie樹 又稱字典樹 單詞查詢樹,是一種樹形結構,用於儲存大量的字串。它的優點是 利用字串的公共字首來節約儲存空間。具體參見 失敗指標 作...

ac自動機 匹配最長字首 多模匹配 AC自動機

精確的字串匹配演算法有 單模匹配演算法,比如,kmp bm演算法等 和 多模匹配演算法,比如,wu manber ac演算法等。ac演算法 aho corasick 是kmp演算法向多模式串情形的擴充套件,該演算法使用一種特殊的自動機,即ac自動機。ac自動機由一組模式串p生成,是trie的擴充套件...