1954 字
8 分钟
AC 自动机

引入#

先推荐一下23级学长warzone 的题解yyb佬的博客

我们用 KMP 解决了一个模式串匹配一个主串的问题,那如果有多个模式串该怎么办?

AC 自动机是一种支持多模式串匹配的数据结构。在构造完成后,可以用 O(S+匹配结果数量)O(|S|+匹配结果数量) 的时间求出若干个模式串 T1,T2,TnT_{1},T_{2}\dots,T_{n}SS 中出现了几次,出现在哪里等问题。

相较于 KMP,简直遥遥领先!(除了空间复杂度略大一点)

暴力解法#

先想当然一下,我们之前学习了 KMP,在此基础上,最好的方法,无非就是对每一个模式串都做一遍 KMP,时间复杂度是 O(nS+1nTi)O\left( n|S|+\sum_{1}^{n} |T_{i}| \right)

对于主串 SS,我们进行了很多重复的查询。有什么优化方法,让我们只查询一次主串就解决问题?

把模式串组合成一个!

AC 自动机#

概述#

结合前面 Trie 的学习,我们很轻松 (并非很轻松) 就能想到使用 Trie 来组合模式串,再在 Trie 上建立失配函数,以实现只查询一次主串就解决问题的目的。

简单来说,建立一个 AC 自动机有两个步骤:

  1. 将所有模式串构建成一棵 Trie。
  2. 对 Trie 上所有结点构造失配指针。

下面是字符串集合 {ab,bca,caa}\{ab,bca,caa\} 构建的 AC 自动机。 AC 自动机示意图

第一步:由字符串集合构建 Trie。具体步骤不再赘述。

第二步:构造失配指针。失配指针的作用和 KMP 中的 fail 数组相同,指向当前字符失配后需要跳转到的结点。 含义也类似。 原本在一个字符串中,fail[i]=j 的含义是 P[0,1,,i1]P[0,1,\dots,i-1] 的最长相等前后缀长度为 jj。由于我个人习惯 0-base,所以这个 jj 恰好也是模式串对应位置的下标。 而在 Trie 上,fail[u] 指向结点 v 的含义是,从 rootrootvv 的结点所表示的字符串,是从 rootrootuu 的结点所表示的字符串的最长真后缀。

说起来有些绕,但是画一画图就很好理解了。

求失配指针#

首先,我们可以确定,每一个结点 uu 的失配指针指向的点的深度,一定比 uu 的深度要小。

也就是说,第二层的结点的失配指针一定指向根节点。

设结点 uu 的父亲 ff 的失配指针指向 vfv_f。 那么如果 vfv_{f} 有和 uu 值(这里我们假定结点的值就是指向结点的边的值)相同的子结点 vv,那么 uu 的失配指针就指向 vv。 如果没有,则继续沿 vfv_f 的失配指针向前,直到找到一个存在相同子结点的结点。 如果跳到根节点仍找不到符合条件的结点,则令 uu 的失配指针指向 rootroot

这里可能难以理解,还是建议画图。

由于我们在处理失配指针时,需要查看父节点的失配指针,所以我们采用 BFS 来实现。

void build() {
    for (auto& node : tree) {
        node.fail = 0;
        node.outputlink = 0;
    }
    queue<int> q;
    for (auto& [c, v] : tree[0].child) {
        tree[v].fail = 0;
        tree[v].outputlink = 0;
        q.push(v);
    }
    while (q.size()) {
        int u = q.front();
        q.pop();
        for (auto& [c, v] : tree[u].child) {
            int p = tree[u].fail;
            while (p != 0 && tree[p].child.find(c) == tree[p].child.end())
                p = tree[p].fail;
            auto pos = tree[p].child.find(c);
            if (pos != tree[p].child.end() && pos->second != v)
                tree[v].fail = pos->second;
            else
                tree[v].fail = 0;
            int f = tree[v].fail;
            if (tree[f].index.size())
                tree[v].outputlink = f;
            else
                tree[v].outputlink = tree[f].outputlink;
            q.push(v);
        }
    }
    built = 1;
}

整体代码#

这里我为了泛用性,导致代码很长,实际使用中只需要按需编写即可,最重要的还是理解算法。

template <typename T> struct AhoCorasick {
    struct Node {
        unordered_map<T, int> child;
        vector<int> index;
        int fail = 0, outputlink = 0;
    };
    vector<Node> tree;
    vector<int> length; // 在模式串 id 不连续,或范围很大时,可以使用 unordered_map
    bool built = 0;
    AhoCorasick() { tree.emplace_back(); }
    template <typename Iterator> void insert(Iterator begin, Iterator end, int id) {
        int u = 0, len = 0;
        for (auto it = begin; it != end; it++, len++) {
            const T& c = *it;
            auto pos = tree[u].child.find(c);
            if (pos == tree[u].child.end()) {
                int v = tree.size();
                tree[u].child[c] = v;
                tree.emplace_back();
                u = v;
            } else
                u = pos->second;
        }
        if (length.size() <= id)
            length.resize(id + 1);
        length[id] = len;
        tree[u].index.push_back(id);
        built = 0;
    }
    void build() {
        for (auto& node : tree) {
            node.fail = 0;
            node.outputlink = 0;
        }
        queue<int> q;
        for (auto& [c, v] : tree[0].child) {
            tree[v].fail = 0;
            tree[v].outputlink = 0;
            q.push(v);
        }
        while (q.size()) {
            int u = q.front();
            q.pop();
            for (auto& [c, v] : tree[u].child) {
                int p = tree[u].fail;
                while (p != 0 && tree[p].child.find(c) == tree[p].child.end())
                    p = tree[p].fail;
                auto pos = tree[p].child.find(c);
                if (pos != tree[p].child.end() && pos->second != v)
                    tree[v].fail = pos->second;
                else
                    tree[v].fail = 0;
                int f = tree[v].fail;
                if (tree[f].index.size())
                    tree[v].outputlink = f;
                else
                    tree[v].outputlink = tree[f].outputlink;
                q.push(v);
            }
        }
        built = 1;
    }
    template <typename Iterator> vector<pair<int, int>> query(Iterator begin, Iterator end) {
        if (!built)
            build();
        vector<pair<int, int>> ans;
        int u = 0, pos = 0;
        for (auto it = begin; it != end; it++, pos++) {
            const T& c = *it;
            while (u != 0 && tree[u].child.find(c) == tree[u].child.end())
                u = tree[u].fail;
            auto poss = tree[u].child.find(c);
            if (poss != tree[u].child.end())
                u = poss->second;
            else
                u = 0;
            for (int id : tree[u].index)
                ans.push_back({ id, pos - length[id] + 1 });
            for (int v = tree[u].outputlink; v != 0; v = tree[v].outputlink) {
                for (int id : tree[v].index)
                    ans.push_back({ id, pos - length[id] + 1 });
            }
        }
        return ans;
    }
};
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

AC 自动机
https://leaf146.cn/posts/ac-自动机
作者
LeAf146
发布于
2026-08-02
许可协议
MIT

部分信息可能已经过时