AIGC标识 AC自动机

下面是一份比赛常用的 AC 自动机 C++ 模板,默认字符集为小写字母 'a' ~ 'z',支持重复模式串,查询每个模式串在文本中的出现次数(可重叠)。

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

struct AhoCorasick {
    static constexpr int SIGMA = 26;

    vector<array<int, SIGMA>> nxt; // Trie 转移
    vector<int> fail;              // fail 指针
    vector<int> cnt;               // 状态访问次数 / 最后作为出现次数
    vector<int> ord;               // BFS 序,用于逆序合并
    vector<int> pos;               // 每个模式串的结束节点

    AhoCorasick() { init(); }

    explicit AhoCorasick(int maxNode) { init(maxNode); }

    void init(int maxNode = 0) {
        nxt.clear();
        fail.clear();
        cnt.clear();
        ord.clear();
        pos.clear();

        if (maxNode > 0) {
            nxt.reserve(maxNode + 1);
            fail.reserve(maxNode + 1);
            cnt.reserve(maxNode + 1);
            ord.reserve(maxNode + 1);
        }

        // 根节点为 0
        nxt.push_back({});
        nxt.back().fill(0);
        fail.push_back(0);
        cnt.push_back(0);
    }

    int newNode() {
        nxt.push_back({});
        nxt.back().fill(0);
        fail.push_back(0);
        cnt.push_back(0);
        return (int)nxt.size() - 1;
    }

    // 插入模式串,返回它的编号
    int insert(const string& s) {
        int u = 0;
        for (char ch : s) {
            int c = ch - 'a';
            if (nxt[u][c] == 0) {
                nxt[u][c] = newNode();
            }
            u = nxt[u][c];
        }
        pos.push_back(u);
        return (int)pos.size() - 1;
    }

    // 构建 fail 指针,并补全 Trie 转移
    void build() {
        queue<int> q;

        // 根节点的子节点
        for (int c = 0; c < SIGMA; ++c) {
            int v = nxt[0][c];
            if (v) {
                fail[v] = 0;
                q.push(v);
            }
        }

        ord.clear();

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

            for (int c = 0; c < SIGMA; ++c) {
                int v = nxt[u][c];

                if (v) {
                    fail[v] = nxt[fail[u]][c];
                    q.push(v);
                } else {
                    // 补全转移,匹配时可以 O(1) 跳转
                    nxt[u][c] = nxt[fail[u]][c];
                }
            }
        }
    }

    // 统计文本 t 中每个模式串的出现次数
    // 调用后用 occurrences(i) 查询第 i 个模式串
    void match(const string& t) {
        fill(cnt.begin(), cnt.end(), 0);

        int u = 0;
        for (char ch : t) {
            int c = ch - 'a';
            u = nxt[u][c];
            ++cnt[u];
        }

        // 逆 BFS 序,沿 fail 树向上合并
        for (int i = (int)ord.size() - 1; i >= 0; --i) {
            int u = ord[i];
            cnt[fail[u]] += cnt[u];
        }
    }

    // 第 id 个模式串的出现次数
    int occurrences(int id) const {
        return cnt[pos[id]];
    }

    // 第 id 个模式串是否出现
    bool exists(int id) const {
        return occurrences(id) > 0;
    }
};

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

    int n;
    cin >> n;

    AhoCorasick ac;

    for (int i = 0; i < n; ++i) {
        string s;
        cin >> s;
        ac.insert(s);
    }

    ac.build();

    string text;
    cin >> text;

    ac.match(text);

    for (int i = 0; i < n; ++i) {
        cout << ac.occurrences(i) << '\n';
    }

    return 0;
}

AC 自动机利用一个 fail 指针来辅助多模式串的匹配.

fail 指针与 KMP 中的 next 指针相比:

共同点:两者同样是在失配的时候用于跳转的指针.
不同点:next 指针求的是最长 Border(即最长的相同前后缀),而 fail 指针指向所有模式串的前缀中匹配当前状态的最长后缀.
因为 KMP 只对一个模式串做匹配,而 AC 自动机要对多个模式串做匹配.有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同.

总结下来,AC 自动机的失配指针指向当前状态的最长后缀状态.

注意:AC 自动机在做匹配时,同一位上可匹配多个模式串.
i he his her hers
image

posted @ 2026-10-02 21:49  haze1231  阅读(11)  评论(0)    收藏  举报