引入
先推荐一下23级学长warzone 的题解和yyb佬的博客。
我们用 KMP 解决了一个模式串匹配一个主串的问题,那如果有多个模式串该怎么办?
AC 自动机是一种支持多模式串匹配的数据结构。在构造完成后,可以用 的时间求出若干个模式串 在 中出现了几次,出现在哪里等问题。
相较于 KMP,简直遥遥领先!(除了空间复杂度略大一点)
暴力解法
先想当然一下,我们之前学习了 KMP,在此基础上,最好的方法,无非就是对每一个模式串都做一遍 KMP,时间复杂度是 。
对于主串 ,我们进行了很多重复的查询。有什么优化方法,让我们只查询一次主串就解决问题?
把模式串组合成一个!
AC 自动机
概述
结合前面 Trie 的学习,我们很轻松 (并非很轻松) 就能想到使用 Trie 来组合模式串,再在 Trie 上建立失配函数,以实现只查询一次主串就解决问题的目的。
简单来说,建立一个 AC 自动机有两个步骤:
- 将所有模式串构建成一棵 Trie。
- 对 Trie 上所有结点构造失配指针。
下面是字符串集合 构建的 AC 自动机。
第一步:由字符串集合构建 Trie。具体步骤不再赘述。
第二步:构造失配指针。失配指针的作用和 KMP 中的 fail 数组相同,指向当前字符失配后需要跳转到的结点。
含义也类似。
原本在一个字符串中,fail[i]=j 的含义是 的最长相等前后缀长度为 。由于我个人习惯 0-base,所以这个 恰好也是模式串对应位置的下标。
而在 Trie 上,fail[u] 指向结点 v 的含义是,从 到 的结点所表示的字符串,是从 到 的结点所表示的字符串的最长真后缀。
说起来有些绕,但是画一画图就很好理解了。
求失配指针
首先,我们可以确定,每一个结点 的失配指针指向的点的深度,一定比 的深度要小。
也就是说,第二层的结点的失配指针一定指向根节点。
设结点 的父亲 的失配指针指向 。 那么如果 有和 值(这里我们假定结点的值就是指向结点的边的值)相同的子结点 ,那么 的失配指针就指向 。 如果没有,则继续沿 的失配指针向前,直到找到一个存在相同子结点的结点。 如果跳到根节点仍找不到符合条件的结点,则令 的失配指针指向 。
这里可能难以理解,还是建议画图。
由于我们在处理失配指针时,需要查看父节点的失配指针,所以我们采用 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; }};部分信息可能已经过时
