AC自动机:高效多模式匹配算法

AC 自动机(Aho-Corasick Automaton)详解

AC 自动机(Aho-Corasick Automaton)是一种高效的多模式字符串匹配算法,由 Alfred Aho 和 Margaret Corasick 在 1975 年提出。它结合了 Trie 树 和 KMP 算法 的思想,能够在 O(n + m + z) 的时间复杂度内完成多模式匹配,其中:

  • n 是主串的长度,
  • m 是所有模式串的总长度,
  • z 是匹配成功的次数。

AC 自动机特别适合 多模式串匹配 场景,例如:

  • 病毒扫描(检测文本中是否包含已知病毒特征码),
  • 关键词过滤(如敏感词检测),
  • DNA 序列分析等。

一、AC 自动机的核心思想

AC 自动机的核心思想是:

  1. Trie 树:将所有模式串构建成一棵 Trie 树,便于快速查找。
  2. 失败指针(Fail Pointer):类似于 KMP 算法的 next 数组,用于在不匹配时跳转,避免从头开始匹配。
  3. 输出函数(Output Function):记录每个节点对应的匹配模式串。

二、AC 自动机的构建步骤

  1. 构建 Trie 树
    将所有模式串插入到 Trie 树中,并为每个节点标记是否为某个模式串的结尾。

示例:
模式串集合:["he", "she", "his", "hers"]

构建的 Trie 树如下:

        (root)
       /  |  \
      h   s   ...
     / \   \
    e   i   h
   /     \   \
  *       s   i
           \   \
            *   r
                 \
                  s
                   *

其中 * 表示该节点是某个模式串的结尾。


  1. 构建失败指针(Fail Pointer)
    失败指针的作用是:当当前字符匹配失败时,跳转到另一个节点继续匹配,类似于 KMP 的 next 数组。

构建规则:

  • 根节点的失败指针:指向 null(或自身)。
  • 根节点的子节点的失败指针:指向根节点(因为如果匹配失败,可以重新从根节点开始匹配)。
  • 其他节点的失败指针: • 如果当前节点的父节点的失败指针有相同字符的子节点,则直接指向该子节点。 • 否则,继续沿着失败指针向上查找,直到找到匹配的子节点或回到根节点。

示例:
"he", "she", "his", "hers" 为例:

  • h 的失败指针指向根节点(因为它是根节点的子节点)。
  • e 的失败指针:h 的失败指针是根节点,根节点没有 e 子节点,所以 e 的失败指针指向根节点。
  • s 的失败指针:h 的失败指针是根节点,根节点有 s 子节点,所以 s 的失败指针指向根节点的 s 子节点。
  • i 的失败指针:h 的失败指针是根节点,根节点没有 i 子节点,所以 i 的失败指针指向根节点。
  • r 的失败指针:s 的失败指针是 hs 子节点,hs 子节点没有 r 子节点,继续向上找 h 的失败指针(根节点),根节点没有 r 子节点,所以 r 的失败指针指向根节点。

  1. 构建输出函数(Output Function)
    输出函数用于记录每个节点对应的匹配模式串。如果某个节点是某个模式串的结尾,或者它的失败指针指向的节点是某个模式串的结尾,则该节点需要输出对应的模式串。

示例:

  • e"he" 的结尾,所以 e 的输出是 "he"
  • s"she""hers" 的结尾,所以 s 的输出是 "she""hers"(需要检查失败指针链)。

三、AC 自动机的匹配过程

  1. 初始化:从 Trie 树的根节点开始,逐个字符匹配主串。
  2. 匹配字符:
    • 如果当前字符匹配成功,则移动到对应的子节点,并检查该节点及其失败指针链是否有输出。 • 如果匹配失败,则沿着失败指针跳转,直到找到匹配的子节点或回到根节点。
  3. 输出匹配结果:每当到达一个节点时,检查该节点及其失败指针链是否有输出,如果有则记录匹配的模式串。

示例:
主串:"ahishers"
模式串集合:["he", "she", "his", "hers"]

匹配过程:

  1. 'a':不匹配,停留在根节点。
  2. 'h':匹配,移动到 h 节点。
  3. 'i':匹配,移动到 i 节点("his" 匹配成功)。
  4. 's':匹配,移动到 s 节点("his""hers" 匹配成功)。
    • 检查 s 的输出:"hers"。 • 检查 s 的失败指针(指向 hs 子节点)的输出:"she"(因为 hs 子节点是 "she" 的结尾)。
  5. 'h':不匹配,沿着 s 的失败指针跳转(回到 hs 子节点的失败指针,即 h 节点)。
  6. 'e':匹配,移动到 e 节点("he" 匹配成功)。

最终匹配结果:"his", "hers", "he"


四、AC 自动机的 C++ 实现

以下是 AC 自动机的完整 C++ 实现:

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;

struct TrieNode {
    unordered_map<char, TrieNode*> children;
    TrieNode* fail = nullptr;
    vector<int> output; // 存储匹配的模式串索引
};

class ACAutomaton {
private:
    TrieNode* root;
    vector<string> patterns;

    void buildTrie(const vector<string>& patterns) {
        this->patterns = patterns;
        root = new TrieNode();
        for (int i = 0; i < patterns.size(); ++i) {
            TrieNode* node = root;
            for (char c : patterns[i]) {
                if (!node->children.count(c)) {
                    node->children[c] = new TrieNode();
                }
                node = node->children[c];
            }
            node->output.push_back(i); // 记录模式串索引
        }
    }

    void buildFailPointers() {
        queue<TrieNode*> q;
        root->fail = nullptr;
        for (auto& [c, child] : root->children) {
            child->fail = root;
            q.push(child);
        }

        while (!q.empty()) {
            TrieNode* current = q.front();
            q.pop();

            for (auto& [c, child] : current->children) {
                TrieNode* failNode = current->fail;
                while (failNode != nullptr && !failNode->children.count(c)) {
                    failNode = failNode->fail;
                }
                child->fail = (failNode == nullptr) ? root : failNode->children[c];
                child->output.insert(child->output.end(), child->fail->output.begin(), child->fail->output.end());
                q.push(child);
            }
        }
    }

public:
    ACAutomaton(const vector<string>& patterns) {
        buildTrie(patterns);
        buildFailPointers();
    }

    vector<int> search(const string& text) {
        vector<int> matches;
        TrieNode* node = root;
        for (int i = 0; i < text.size(); ++i) {
            char c = text[i];
            while (node != nullptr && !node->children.count(c)) {
                node = node->fail;
            }
            if (node == nullptr) {
                node = root;
                continue;
            }
            node = node->children[c];
            for (int patternIndex : node->output) {
                matches.push_back(patternIndex);
            }
        }
        return matches;
    }

    vector<string> getPatterns() const {
        return patterns;
    }
};

int main() {
    vector<string> patterns = {"he", "she", "his", "hers"};
    ACAutomaton ac(patterns);

    string text = "ahishers";
    vector<int> matches = ac.search(text);

    cout << "Matches found at positions: ";
    for (int idx : matches) {
        cout << patterns[idx] << " ";
    }
    cout << endl;

    return 0;
}

五、AC 自动机的时间复杂度

  1. 构建 Trie 树:O(m),其中 m 是所有模式串的总长度。
  2. 构建失败指针:O(m),使用 BFS 遍历 Trie 树。
  3. 匹配过程:O(n + z),其中 n 是主串长度,z 是匹配成功的次数。

总时间复杂度:O(m + n + z)。


六、AC 自动机的应用场景

  1. 病毒扫描:检测文本中是否包含已知病毒特征码。
  2. 关键词过滤:如敏感词检测、广告屏蔽。
  3. DNA 序列分析:快速查找 DNA 序列中的特定模式。
  4. 搜索引擎:快速匹配搜索关键词。

七、总结

AC 自动机是一种高效的多模式字符串匹配算法,结合了 Trie 树和 KMP 算法的思想,能够在 O(n + m + z) 的时间复杂度内完成匹配。它的核心是 Trie 树 和 失败指针,适用于需要同时匹配多个模式串的场景。

滚动至顶部