AC 自动机CSP-S

目录

💡 核心思想

AC 自动机(Aho-Corasick)是多模式串匹配的自动化算法。它先把所有模式串建成 Trie 树,然后通过 BFS 构建失败指针(fail),使得在匹配过程中失配时可以快速跳转到下一个可能的匹配位置。一次扫描主串,即可同时匹配所有模式串,时间复杂度 O(|主串| + |模式串总长| + 匹配次数)。

AC 自动机是 KMP 的多串推广。很多选手看到"多模式串匹配"就用 KMP 逐个匹配,复杂度是 O(|主串| × |模式串数量|)。AC 自动机直接把复杂度降到线性。构建过程分两步:建 Trie + BFS 求 fail。理解 fail 指针的含义是关键:fail[v] 表示 Trie 中以 v 结尾的字符串的最长真后缀,且这个后缀也是某个模式串的前缀。

🎯 直觉理解

想象你在一个巨大的文本中查找多个关键词。AC 自动机就像一个"智能扫描仪":它先记住所有关键词的形状(Trie 树),然后一边读文本一边匹配。如果匹配失败了,它不是从头再来,而是根据 fail 指针跳转到最接近的备选位置——就像你在打字时打错了一个字母,不需要把整个词删掉重打,而是根据词典提示跳到最可能的修正位置。

📝 算法流程

  1. 将所有模式串插入 Trie 树
  2. BFS 构建 fail 指针:
  3. 根节点的子节点的 fail 指向根
  4. 对于节点 u 和字符 c:如果 u 有 c 子节点 v,则 fail[v] = go(fail[u], c)
  5. 如果 u 没有 c 子节点,则 go(u, c) = go(fail[u], c)(自动机转移)
  6. 在主串上按自动机转移,每到一个节点就输出该节点及其 fail 链上的所有模式串

$$\text{时间:}O(|S| + \sum |P_i| + \text{匹配次数})$$

📊 复杂度分析

指标复杂度
时间$O(|S| + \sum |P_i|)$ 构建 / $O(|S| + \text{匹配次数})$ 查询
空间$O(|\Sigma| \cdot \sum |P_i|)$

💻 参考实现(C++)

C++ (C++17)
#include 
using namespace std;

struct ACAutomaton {
    static const int SIGMA = 26;
    struct Node {
        int next[SIGMA];
        int fail;
        int count; // 以该节点结尾的模式串数量
        Node() { memset(next, 0, sizeof(next)); fail = 0; count = 0; }
    };
    vector nodes;
    
    ACAutomaton() { nodes.emplace_back(); } // root = 0
    
    void insert(const string& s) {
        int u = 0;
        for (char c : s) {
            int idx = c - 'a';
            if (!nodes[u].next[idx]) {
                nodes[u].next[idx] = nodes.size();
                nodes.emplace_back();
            }
            u = nodes[u].next[idx];
        }
        nodes[u].count++;
    }
    
    void build() {
        queue q;
        for (int c = 0; c < SIGMA; c++)
            if (nodes[0].next[c]) q.push(nodes[0].next[c]);
        
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int c = 0; c < SIGMA; c++) {
                int v = nodes[u].next[c];
                if (v) {
                    nodes[v].fail = nodes[nodes[u].fail].next[c];
                    nodes[v].count += nodes[nodes[v].fail].count;
                    q.push(v);
                } else {
                    nodes[u].next[c] = nodes[nodes[u].fail].next[c];
                }
            }
        }
    }
    
    int query(const string& s) {
        int u = 0, res = 0;
        for (char c : s) {
            u = nodes[u].next[c - 'a'];
            res += nodes[u].count;
        }
        return res;
    }
};

int main() {
    ACAutomaton ac;
    ac.insert("he");
    ac.insert("she");
    ac.insert("his");
    ac.insert("hers");
    ac.build();
    cout << ac.query("ushers") << endl; // 3 ("she", "he", "hers")
    return 0;
}

⚠️ 常见坑点

fail 指针构建顺序——必须 BFS,不能 DFS

节点编号从 0 还是 1 开始——建议从 0(根节点)

模式串重复——count 要累加,不能只设为 1

转移数组初始化——没有子节点时要指向 fail 的对应子节点

📚 相关题目

题目来源难度备注
P3808 模板 AC 自动机洛谷CSP-SAC 自动机模板题
P3796 单词出现洛谷CSP-SAC 自动机 + 拓扑排序
P5357 模板 AC 自动机(二次加强版)洛谷CSP-SAC 自动机 + 线段树