AC 自动机详细解析

AC自动机解析

前置知识

Trie 字典树 KMP

建议彻底理解 Trie,这很重要。

介绍

AC 自动机(ACAM)是一种实现多模匹配的自动机(Automaton),将 KMP 思想和 Trie 融合起来,可以在 \(O(|s|+\sum|t_i|)\) 的时间,\(O(|s|+\sum|t_i|)\) 的空间内完成多模匹配。

原理

Fail 指针

这里的 Fail 指针和 KMP 的失配树中的 \(next\) 数组还是有区别的。KMP 的 \(next\) 指针是 Border,也就是最长相同真前后缀。而这里的 Fail 指针是指向最长的在 Trie 里的真后缀。

匹配

示意图:

其中插入的模式串为:\(t_1=\texttt{AB},t_2=\texttt{ABB},t_3=\texttt{ABC},t_4=\texttt{BC},t_5=\texttt{CA},t_6=\texttt{CBB},t_7=\texttt{CC}\),其中圈出的为 Trie 树上的标记。文本串是 \(s=\texttt{ABBCABACBCC}\)。

如果直接匹配就会发现当匹配到第三个字符时,刚好匹配上,此时匹配次数加一。然后呢?只能从根继续匹配。

我们想到优化:我们可以直接从最长的在 Trie 里的真后缀开始继续匹配,因为这样可以省去了前面的匹配,直接从 Fail 指针处继续。

如果没有在 Trie 里的真后缀怎么办?这就要从根开始了,也就是从头匹配。

匹配失败了也一样,跳 fail 直到回到根。

算法流程

洛谷 P3808 AC 自动机(简单版)为例讲解。

建树

就是 Trie 本身的建树。不会可以去搜索一下。

代码:

cpp 复制代码
void insert(const string &t) {
	int now = 1;
    for(auto &c : t) {
        if(!trie[now].son[c - 'a']) trie[now].son[c - 'a'] = ++tc;
        now = trie[now].son[c - 'a'];
    }
    trie[now].flg++;
}

求 fail 指针

方便表述,记 \(Fail_u\) 是 \(u\) 的 fail 指向的节点,\(S_u\) 是 \(u\) 指向的串。

我们可以发现两个性质:

  • 真后缀的的真后缀是真后缀。
  • 如果一个节点 \(u\) 和 \(Fail_u\) 都有一个相同儿子 \(i,i\in\{\texttt{A},\dots,\texttt{Z}\}\),那么 \(S_{Fail_u}+i\)( \(+\) 表示字符串拼接)就是\(s_u+i\) 的最长的在 Trie 里的真后缀。

所以我们就可以从深度小的串求出深度大的串的 fail 指针。我们发现这就类似 Trie 上 BFS。具体流程如下:

  1. 在访问到一个节点 \(u\) 的时候,遍历所有存在的子节点。
  2. 如果 \(u\) 的 fail 也有这个子节点,就直接将这个儿子的 fail 指向 \(u\) 的 fail 的这个子节点。否则重复跳直到根。

我们考虑继续优化。这里有一个技巧:路径压缩。

我们可以发现 \(Fail_u\) 总是在 \(u\) 之前计算,那我们就可以直接不管对方有没有这个子节点,就直接挂上去,也就是挂到 \(Fail_u\) 跳 fail 之后的节点。这就可以避免构建时每次跳 fail。

同样的,在 \(u\) 本来就没有的子节点,就可以直接挂到 \(Fail_u\) 的子节点上。这样在失配的时候就可以直接跳 fail 了。

为了搞定根节点的情况,我们规定 \(Fail_{root} = 0\),\(0\) 的所有子节点都是根。这样我们在更新第一层的时候,fail 就是 \(0\) 的儿子也就是根。

代码:

cpp 复制代码
void build() {
    queue<int> q;
    for(int i = 0; i < 26; i++) trie[0].son[i] = 1;
    q.push(1), trie[1].fail = 0;
    while(!q.empty()) {
        int fa = q.front(); q.pop();
        for(int i = 0; i < 26; i++) {
            int &son = trie[fa].son[i];
            int fafail = trie[fa].fail;
            if(!son) {son = trie[fafail].son[i]; continue;} // 没有的子节点直接跳 fail
            trie[son].fail = trie[fafail].son[i]; // 直接挂上去即可
            q.push(son);
        }
    }
}

查询

其实就是前面的匹配,但加入了路径压缩,在失配时就可以直接跳了。

cpp 复制代码
int query(const string &s) {
    int ans = 0, u = 1;
    for(auto &_ : s) {
        int c = _ - 'a', v = trie[u].son[c];
        while(v > 1 && trie[v].flg != -1) { // 不断跳 fail,算过就不算了
            ans += trie[v].flg; trie[v].flg = -1;
            v = trie[v].fail;
        }
        u = trie[u].son[c];
    }
    return ans;
}

代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 5;
struct ACAM {
    struct Node {
        int son[26], fail, flg;
    } trie[N];
    int tc = 1;
    void insert(const string &t) {
        int now = 1;
        for(auto &c : t) {
            if(!trie[now].son[c - 'a']) trie[now].son[c - 'a'] = ++tc;
            now = trie[now].son[c - 'a'];
        }
        trie[now].flg++;
    }
    void build() {
        queue<int> q;
        for(int i = 0; i < 26; i++) trie[0].son[i] = 1;
        q.push(1), trie[1].fail = 0;
        while(!q.empty()) {
            int fa = q.front(); q.pop();
            for(int i = 0; i < 26; i++) {
                int &son = trie[fa].son[i];
                int fafail = trie[fa].fail;
                if(!son) {son = trie[fafail].son[i]; continue;}
                trie[son].fail = trie[fafail].son[i];
                q.push(son);
            }
        }
    }
    int query(const string &s) {
        int ans = 0, u = 1;
        for(auto &_ : s) {
            int c = _ - 'a', v = trie[u].son[c];
            while(v > 1 && trie[v].flg != -1) {
                ans += trie[v].flg; trie[v].flg = -1;
                v = trie[v].fail;
            }
            u = trie[u].son[c];
        }
        return ans;
    }
}acam;
int main() {
    ios::sync_with_stdio(0), cin.tie(0);
    int n; cin >> n;
    for(int i = 1; i <= n; i++) {
        string t; cin >> t;
        acam.insert(t);
    }
    acam.build();
    string s; cin >> s;
    cout << acam.query(s) << "\n";
    return 0;
}