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

AC自动机Trie加失败指针一次扫描命中全部模式串

什么是 AC 自动机

AC 自动机(Aho-Corasick)1975 年由 Alfred Aho 和 Margaret Corasick 提出,解决多模式匹配:给定一组模式串,一次扫描主串找出所有模式串的所有出现位置,复杂度 O(n + m + z)(n 主串长、m 模式串总长、z 命中次数)。

适用场景:敏感词过滤、病毒特征码扫描、DNA 序列多模式查找、代码扫描器。

三件套:Trie + 失败指针 + 输出

  • Trie 树:所有模式串插进一棵字典树,公共前缀共享路径;
  • 失败指针(fail):类似 KMP 的 next——当前节点无对应子边时,沿失败指针跳到”最长真后缀”对应的节点,不回到根重来;
  • 输出(output):每个节点记录”走到这里命中哪些模式串”。
AC自动机Trie结构与失败指针:he/she/his/hers
图 1:模式串 he/she/his/hers 的 Trie 与失败指针(虚线)

失败指针的语义:节点代表一个前缀串,它的 fail 指向该串的最长真后缀对应的节点。比如 “hers” 的 s 节点,最长真后缀是 “s”(”she” 的开头),所以 fail 指向 she 的 s 节点——匹配 her 后失配,还能顺带用上已经扫过的后缀。

构建步骤

1. 建 Trie

把每个模式串逐字符插入,末尾节点记下模式串编号。复杂度 O(m)。

2. BFS 建失败指针

规则:根的子节点 fail 指向根;其余节点,看父节点的 fail 是否有同字符子节点——有就指向它,没有就继续沿 fail 链向上,直到根。逐层 BFS,每个节点只入队一次,O(m)。

3. 合并输出

一个节点的输出 = 自身结尾的模式串 ∪ 失败指针链上所有节点的输出。建 fail 时直接把 fail 节点的输出并进来,匹配时就不用再跳链收集。

匹配过程

从根开始逐个读主串字符:有子边走子边;没有就沿 fail 跳,跳到根还没有,则从根重新开始(当前字符匹配不上)。

AC自动机匹配主串ahishers的状态推进与输出
图 2:扫描 “ahishers” 的状态推进——每字符 O(1) 步,命中点输出全部模式串

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

'a': 根没有 a 子边,停在根
'h': 进入 h 节点
'i': 进入 i 节点
's': 进入 s 节点,输出 "his";沿失败链到 s(she),输出 "she"
'h': s(his) 无 h 子边,沿失败指针到 s(she),再走 h 子边进入 h
'e': 进入 e 节点,输出 "she";失败链上的 e(he) 输出 "he"
'r': e(she) 无 r 子边,沿失败指针到 e(he),走 r 子边进入 r
's': 进入 s 节点,输出 "hers";失败链上的 s(she) 输出 "she"
命中:his(1-3)、she(4-5 与 5-6)、he(5-6)、hers(4-7)

完整代码(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>& ps) {
        patterns = ps;
        root = new TrieNode();
        for (int i = 0; i < (int)ps.size(); ++i) {
            TrieNode* node = root;
            for (char c : ps[i]) {
                if (!node->children.count(c))
                    node->children[c] = new TrieNode();
                node = node->children[c];
            }
            node->output.push_back(i);
        }
    }

    void buildFail() {
        queue<TrieNode*> q;
        for (auto& [c, child] : root->children) {
            child->fail = root;
            q.push(child);
        }
        while (!q.empty()) {
            TrieNode* cur = q.front(); q.pop();
            for (auto& [c, child] : cur->children) {
                TrieNode* f = cur->fail;
                while (f && !f->children.count(c)) f = f->fail;
                child->fail = (f == nullptr) ? root : f->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);
        buildFail();
    }

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

int main() {
    ACAutomaton ac({"he", "she", "his", "hers"});
    for (int idx : ac.search("ahishers"))
        cout << "match: " << idx << "n";
    return 0;
}

复杂度与总结

  • 建 Trie O(m),建失败指针 O(m),匹配 O(n + z)——总 O(n + m + z),与模式串个数无关;
  • 对比”每个模式串单独跑 KMP”的 O(k·n),模式串多时优势明显;
  • AC 自动机是”Trie 组织模式串 + 失败指针复用匹配信息”的经典组合,多模式匹配场景的首选。
滚动至顶部