AC 自动机(简单版)

GitHub跳转原题关系图返回列表

把所有模式串插入 Trie 并建立 fail 指针,扫描文本串时沿 fail 链统计出现过的终止节点。

OJ: luogu

题目 ID: P3808

难度:普及/提高-

标签:字符串字典树AC自动机模板题

日期: 2026-07-06 23:57

题意

给定 n 个模式串和一个文本串 t,要求统计有多少个模式串在 t 中出现过。

注意:题目按“编号”区分模式串。
如果两个模式串内容完全相同,但编号不同,只要这个字符串在文本中出现,它们都要分别计数。

思路

最直接的做法,是逐个模式串去文本串里找。

下面这个暴力按编号逐个检查,所以重复模式串也能正确计数:

cpp
// brute.cpp:小数据暴力解,逐个模式串检查它是否在文本串中出现。
#include <bits/stdc++.h>
using namespace std;

int n;
string pattern_list[105];
string text;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> pattern_list[i];
    }
    cin >> text;

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        bool found = false;
        int len = (int)pattern_list[i].size();
        for (int l = 0; l + len <= (int)text.size(); l++) {
            bool same = true;
            for (int j = 0; j < len; j++) {
                if (text[l + j] != pattern_list[i][j]) {
                    same = false;
                    break;
                }
            }
            if (same) {
                found = true;
                break;
            }
        }
        if (found) ans++;
    }

    cout << ans << "\n";

    return 0;
}

但总模式串长度和文本串长度都能达到 10^6,逐个查找会超时。

我们需要一次扫描文本串,同时匹配所有模式串。这正是 AC 自动机的用途。

第一步:建 Trie

把所有模式串插入 Trie。

每个终止节点记录:

text
end_count[u] = 有多少个模式串编号在 u 这个节点结尾

如果两个模式串内容相同,它们会落在同一个终止节点,但 end_count[u] 会累加两次。

第二步:建 fail 指针

fail[u] 表示:当前状态匹配失败时,应该跳到哪个最长后缀状态继续匹配。

用 BFS 建 fail 指针时,同时补全缺失转移:

  • 如果 u 有字符 c 的儿子,就给这个儿子设置 fail;
  • 如果没有,就让 trie[u][c] = trie[fail[u]][c]

这样扫描文本串时,每个字符都能直接转移到下一个状态。

第三步:扫描文本串

扫描到文本串某个位置时,当前状态 u 表示“以当前位置结尾的某个后缀”。

沿着 u 的 fail 链向上走,链上的所有终止节点,都代表一个以当前位置结尾的模式串。

统计后把节点标记为 -1,表示这个终止节点已经贡献过答案。这样同一个模式串编号即使在文本中出现多次,也只会被统计一次。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXNODE = 1000005;
const int SIGMA = 26;

int n;
int trie[MAXNODE][SIGMA];
int fail_link[MAXNODE];
int end_count[MAXNODE]; // end_count[u] 表示有多少个模式串在 u 结尾
int node_cnt;
string text;

void insert_pattern(const string &s) {
    int u = 0;
    for (int i = 0; i < (int)s.size(); i++) {
        int c = s[i] - 'a';
        if (trie[u][c] == 0) {
            node_cnt++;
            trie[u][c] = node_cnt;
        }
        u = trie[u][c];
    }
    end_count[u]++;
}

void build_ac_automaton() {
    queue<int> q;
    for (int c = 0; c < SIGMA; c++) {
        int v = trie[0][c];
        if (v != 0) {
            fail_link[v] = 0;
            q.push(v);
        }
    }

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        for (int c = 0; c < SIGMA; c++) {
            int v = trie[u][c];
            if (v != 0) {
                fail_link[v] = trie[fail_link[u]][c];
                q.push(v);
            } else {
                // 补全转移:失配时直接跳到 fail 后能走到的状态。
                trie[u][c] = trie[fail_link[u]][c];
            }
        }
    }
}

long long count_matched_patterns() {
    long long ans = 0;
    int u = 0;

    for (int i = 0; i < (int)text.size(); i++) {
        int c = text[i] - 'a';
        u = trie[u][c];

        // 沿 fail 链统计所有以当前位置结尾的模式串。
        // 统计过的节点标成 -1,保证同一个模式串编号只贡献一次。
        int p = u;
        while (p != 0 && end_count[p] != -1) {
            ans += end_count[p];
            end_count[p] = -1;
            p = fail_link[p];
        }
    }

    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        insert_pattern(s);
    }
    cin >> text;

    build_ac_automaton();
    cout << count_matched_patterns() << "\n";

    return 0;
}

复杂度

设所有模式串总长度为 L,文本串长度为 T

建 Trie、建 fail 指针和扫描文本串都在总长度量级内完成;补全转移时每个节点枚举 26 个小写字母。

时间复杂度:O(26L+T)O(26L + T)

空间复杂度:O(26L)O(26L)

总结

AC 自动机可以理解为:

text
Trie + fail 指针

Trie 负责保存所有模式串,fail 指针负责在匹配失败时跳到最长可用后缀。

本题还要特别注意“按编号计数”:重复字符串不能去重,而是要在终止节点累计数量。