跳到主要内容
C++ AC 自动机:原理、实现与应用 | 极客日志
C++ 算法
C++ AC 自动机:原理、实现与应用 综述由AI生成 AC 自动机是一种结合字典树和 KMP 算法的高效多模式匹配算法,用于在文本中同时匹配多个关键词。其核心在于利用失败指针实现失配后的快速回退,将匹配时间复杂度降为线性。详细阐述了 AC 自动机的数据结构设计、构建流程(Trie 插入、BFS 建 Fail 指针、文本匹配),提供了完整的 C++ 实现代码,并探讨了字符集扩展、出现次数统计及路径压缩等优化方案,最后总结了敏感词过滤、日志分析等典型应用场景。
邪神洛基 发布于 2026/3/15 更新于 2026/6/2 24 浏览C++ AC 自动机:原理、实现与应用
AC 自动机(Aho-Corasick Automaton)是结合字典树(Trie)和 KMP 算法 思想的高效多模式匹配算法,核心解决'在一段文本中同时匹配多个模式串(关键词)'的问题。其优势在于:预处理模式串的时间复杂度为 O(∑len)(∑len 为所有模式串总长度),文本匹配的时间复杂度为 O(n)(n 为文本长度),远优于暴力匹配(O(n · ∑len))。本文将从核心原理、结构设计、构建流程到实战应用,全面解析 AC 自动机的设计思想与 C++ 实现技巧。
一、AC 自动机的核心背景与优势
1.1 问题引入:多模式匹配的瓶颈
在文本处理场景(如敏感词过滤、日志关键词提取)中,需同时匹配数十/数百个模式串,传统方法存在明显缺陷:
暴力匹配 :对每个模式串单独用 KMP 匹配文本,总时间复杂度 O(n · k)(k 为模式串数量),效率极低。
普通 Trie :仅能高效处理前缀匹配,无法处理'失配后回退'的场景(如文本字符不匹配时,需重新从根节点开始匹配)。
AC 自动机的核心改进:
失败指针(Fail 指针) :借鉴 KMP 的部分匹配表(next 数组),为 Trie 每个节点添加失败指针,失配时快速回退到最长公共后缀节点,避免重新从头匹配。
多模式批量匹配 :一次遍历文本即可匹配所有模式串,效率远超传统方法。
1.2 核心概念:AC 自动机的三大组件
AC 自动机基于 Trie 扩展,包含三个核心部分:
Trie 结构 :存储所有模式串的公共前缀,是匹配的基础。
失败指针(Fail 指针) :每个节点的失败指针指向'当前节点的最长后缀对应的 Trie 节点',失配时跳转。
输出链表 :记录当前节点对应的所有模式串(如节点对应 app,输出链表包含 app;若同时对应 p,则包含 p)。
示例 :模式串为 he、she、his、hers 的 AC 自动机结构(简化):
根节点(fail =null) ├─ h (fail=根)
│ ├─ e (fail =根->e,输出:he)
│ │ └─ r (fail =根)
│ │ └─ s (fail =根->s,输出:hers)
│ └─ i (fail =根)
│ └─ s (fail =根->s,输出:his)
├─ s (fail =根)
│ └─ h (fail =根->h)
│ └─ e (fail =根->h->e,输出:she)
└─ e (fail =根)
二、AC 自动机的核心结构设计
2.1 节点(ACNode)的定义 AC 自动机的节点在 Trie 节点基础上,新增 失败指针 和 输出标记 (记录是否为模式串结尾,或存储模式串列表):
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
struct ACNode {
int children[26 ];
int fail;
int len;
ACNode () {
memset (children, -1 , sizeof (children));
fail = -1 ;
len = 0 ;
}
};
索引替代指针 :用数组存储所有节点,通过索引访问(避免指针的内存管理问题,更高效)。
len 标记 :若 len > 0,表示当前节点是一个模式串的结尾,len 为该模式串的长度(便于匹配时快速提取关键词)。
2.2 AC 自动机的整体结构 AC 自动机包含三个核心部分:节点数组(存储所有节点)、根节点索引、队列(用于 BFS 构建失败指针):
class ACAutomaton {
private :
vector<ACNode> nodes;
int root;
int newNode () {
nodes.emplace_back ();
return nodes.size () - 1 ;
}
public :
ACAutomaton () { root = newNode (); }
void insert (const string& pattern) ;
void buildFail () ;
vector<pair<int , int >> match (const string& text);
};
三、AC 自动机的核心构建流程 AC 自动机的构建分为两步:插入所有模式串构建 Trie → BFS 遍历构建失败指针 。
3.1 步骤 1:插入模式串(构建 Trie) 与普通 Trie 的插入逻辑一致,逐个字符插入,最后标记模式串结尾:
void ACAutomaton::insert (const string& pattern) {
int cur = root;
for (char c : pattern) {
int idx = c - 'a' ;
if (nodes[cur].children[idx] == -1 ) {
nodes[cur].children[idx] = newNode ();
}
cur = nodes[cur].children[idx];
}
nodes[cur].len = pattern.size ();
}
示例 :插入 he → 根→h→e,e 节点的 len=2;插入 she → 根→s→h→e,e 节点的 len=2。
3.2 步骤 2:BFS 构建失败指针(核心!)
根节点的子节点 :失败指针指向根节点。
普通节点 u :设其父节点为 p,p 通过字符 c 指向 u;找到 p 的失败指针 fail_p,若 fail_p 有字符 c 的子节点 v,则 u 的失败指针指向 v;否则继续找 fail_p 的失败指针,直到根节点。
失败指针的继承性 :若节点 u 的失败指针指向 v,则 v 的输出(模式串)也属于 u 的匹配结果(需在匹配时回溯)。
void ACAutomaton::buildFail () {
queue<int > q;
for (int i = 0 ; i < 26 ; ++i) {
int child = nodes[root].children[i];
if (child != -1 ) {
nodes[child].fail = root;
q.push (child);
}
}
while (!q.empty ()) {
int p = q.front ();
q.pop ();
for (int i = 0 ; i < 26 ; ++i) {
int u = nodes[p].children[i];
if (u == -1 ) continue ;
int fail_p = nodes[p].fail;
while (fail_p != -1 && nodes[fail_p].children[i] == -1 ) {
fail_p = nodes[fail_p].fail;
}
if (fail_p == -1 ) {
nodes[u].fail = root;
} else {
nodes[u].fail = nodes[fail_p].children[i];
}
q.push (u);
}
}
}
关键逻辑 :失败指针的构建是'层序遍历'(BFS),确保父节点的失败指针先于子节点构建完成。
3.3 步骤 3:文本匹配(核心应用) 匹配逻辑:从根节点开始遍历文本,利用失败指针快速回退,同时收集所有匹配的模式串:
vector<pair<int , int >> ACAutomaton::match (const string& text) {
vector<pair<int , int >> res;
int cur = root;
for (int i = 0 ; i < text.size (); ++i) {
int idx = text[i] - 'a' ;
while (cur != root && nodes[cur].children[idx] == -1 ) {
cur = nodes[cur].fail;
}
if (nodes[cur].children[idx] != -1 ) {
cur = nodes[cur].children[idx];
}
int temp = cur;
while (temp != root) {
if (nodes[temp].len > 0 ) {
int start = i - nodes[temp].len + 1 ;
res.emplace_back (start, nodes[temp].len);
}
temp = nodes[temp].fail;
}
}
return res;
}
示例 :文本 ushers,匹配模式串 he、she、his、hers:
遍历到 s(i=0)→ 无匹配;
遍历到 h(i=1)→ 无匹配;
遍历到 e(i=2)→ 匹配 he(起始 0,长度 2)、she(起始 1,长度 2);
遍历到 r(i=3)→ 无匹配;
遍历到 s(i=4)→ 匹配 hers(起始 1,长度 4);
遍历到 s(i=5)→ 无匹配。
四、完整可运行代码 #include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <cstring>
#include <utility>
using namespace std;
struct ACNode {
int children[26 ];
int fail;
int len;
ACNode () {
memset (children, -1 , sizeof (children));
fail = -1 ;
len = 0 ;
}
};
class ACAutomaton {
private :
vector<ACNode> nodes;
int root;
int newNode () {
nodes.emplace_back ();
return nodes.size () - 1 ;
}
public :
ACAutomaton () { root = newNode (); }
void insert (const string& pattern) {
int cur = root;
for (char c : pattern) {
int idx = c - 'a' ;
if (nodes[cur].children[idx] == -1 ) {
nodes[cur].children[idx] = newNode ();
}
cur = nodes[cur].children[idx];
}
nodes[cur].len = pattern.size ();
}
void buildFail () {
queue<int > q;
for (int i = 0 ; i < 26 ; ++i) {
int child = nodes[root].children[i];
if (child != -1 ) {
nodes[child].fail = root;
q.push (child);
}
}
while (!q.empty ()) {
int p = q.front ();
q.pop ();
for (int i = 0 ; i < 26 ; ++i) {
int u = nodes[p].children[i];
if (u == -1 ) continue ;
int fail_p = nodes[p].fail;
while (fail_p != -1 && nodes[fail_p].children[i] == -1 ) {
fail_p = nodes[fail_p].fail;
}
nodes[u].fail = (fail_p == -1 ) ? root : nodes[fail_p].children[i];
q.push (u);
}
}
}
vector<pair<int , int >> match (const string& text) {
vector<pair<int , int >> res;
int cur = root;
for (int i = 0 ; i < text.size (); ++i) {
if (!islower (text[i])) {
cur = root;
continue ;
}
int idx = text[i] - 'a' ;
while (cur != root && nodes[cur].children[idx] == -1 ) {
cur = nodes[cur].fail;
}
if (nodes[cur].children[idx] != -1 ) {
cur = nodes[cur].children[idx];
}
int temp = cur;
while (temp != root) {
if (nodes[temp].len > 0 ) {
int start = i - nodes[temp].len + 1 ;
res.emplace_back (start, nodes[temp].len);
}
temp = nodes[temp].fail;
}
}
return res;
}
};
int main () {
ACAutomaton ac;
vector<string> patterns = {"he" , "she" , "his" , "hers" };
for (const string& p : patterns) {
ac.insert (p);
}
ac.buildFail ();
string text = "ushers" ;
vector<pair<int , int >> res = ac.match (text);
cout << "文本:" << text << endl;
cout << "匹配结果(起始位置,长度):" << endl;
for (auto & [start, len] : res) {
cout << "位置 " << start << ":" << text.substr (start, len) << endl;
}
return 0 ;
}
文本:ushers
匹配结果(起始位置,长度):
位置 1 :he
位置 0 :she
位置 1 :hers
五、AC 自动机的扩展与优化
5.1 扩展 1:支持任意字符集(替代固定 26 数组) 若需处理大写字母、数字、符号等,将 children[26] 改为 unordered_map<char, int>:
struct ACNode {
unordered_map<char , int > children;
int fail;
int len;
ACNode () : fail (-1 ), len (0 ) {}
};
void insert (const string& pattern) {
int cur = root;
for (char c : pattern) {
if (!nodes[cur].children.count (c)) {
nodes[cur].children[c] = newNode ();
}
cur = nodes[cur].children[c];
}
nodes[cur].len = pattern.size ();
}
void buildFail () {
queue<int > q;
for (auto & [c, child] : nodes[root].children) {
nodes[child].fail = root;
q.push (child);
}
while (!q.empty ()) {
int p = q.front ();
q.pop ();
for (auto & [c, u] : nodes[p].children) {
int fail_p = nodes[p].fail;
while (fail_p != -1 && !nodes[fail_p].children.count (c)) {
fail_p = nodes[fail_p].fail;
}
nodes[u].fail = (fail_p == -1 ) ? root : nodes[fail_p].children[c];
q.push (u);
}
}
}
5.2 扩展 2:统计模式串出现次数 在节点中新增 count 属性,记录模式串出现次数:
struct ACNode {
int children[26 ];
int fail;
int len;
int count;
ACNode () {
memset (children, -1 , sizeof (children));
fail = -1 ;
len = 0 ;
count = 0 ;
}
};
vector<pair<string, int >> countPattern (const string& text, const vector<string>& patterns) {
unordered_map<int , vector<string>> len2pattern;
for (const string& p : patterns) {
len2pattern[p.size ()].push_back (p);
}
auto res = match (text);
unordered_map<string, int > cnt;
for (auto & [start, len] : res) {
string sub = text.substr (start, len);
cnt[sub]++;
}
vector<pair<string, int >> ret;
for (const string& p : patterns) {
ret.emplace_back (p, cnt[p]);
}
return ret;
}
5.3 优化:路径压缩(Fail 树优化) 匹配时需回溯失败指针收集结果,可预先将失败指针的输出合并到当前节点(路径压缩),避免多次回溯:
void compress () {
queue<int > q;
q.push (root);
while (!q.empty ()) {
int u = q.front ();
q.pop ();
if (nodes[u].fail != -1 && nodes[u].fail != root) {
if (nodes[u].len == 0 ) {
nodes[u].len = nodes[nodes[u].fail].len;
}
}
for (int i = 0 ; i < 26 ; ++i) {
int child = nodes[u].children[i];
if (child != -1 ) {
q.push (child);
}
}
}
}
六、AC 自动机的典型应用场景
6.1 敏感词过滤 将所有敏感词存入 AC 自动机,遍历文本匹配敏感词,实现高效替换/屏蔽:
string filterSensitiveWord (const string& text, const vector<string>& sensitiveWords, char replace = '*' ) {
ACAutomaton ac;
for (const string& word : sensitiveWords) {
ac.insert (word);
}
ac.buildFail ();
auto res = ac.match (text);
string filtered = text;
for (auto & [start, len] : res) {
fill (filtered.begin () + start, filtered.begin () + start + len, replace);
}
return filtered;
}
int main () {
vector<string> sensitive = {"赌博" , "色情" , "毒品" };
string text = "禁止赌博和色情内容,远离毒品!" ;
cout << filterSensitiveWord (text, sensitive) << endl;
return 0 ;
}
6.2 日志关键词提取 从海量日志中快速提取所有预设关键词,用于日志分析、告警:
int main () {
vector<string> keywords = {"error" , "warning" , "timeout" };
ACAutomaton ac;
for (const string& kw : keywords) {
ac.insert (kw);
}
ac.buildFail ();
string log = "2026-01-11 10:00:00 [error] connection timeout | 10:01:00 [warning] low memory" ;
auto res = ac.match (log);
cout << "日志中匹配的关键词:" << endl;
for (auto & [start, len] : res) {
cout << log.substr (start, len) << endl;
}
return 0 ;
}
6.3 多模式串匹配(算法竞赛) 解决 LeetCode 3016. 输入单词需要的最少按键次数 II、LeetCode 1032. 字符流等问题,AC 自动机是最优解。
七、常见错误与最佳实践
7.1 常见错误
字符集处理不当 :仅支持小写字母,处理大写/符号时直接崩溃 → 解决方案:扩展字符集为 unordered_map,或先统一转换为小写。
失败指针构建遗漏 :未 BFS 遍历所有节点,导致部分节点失败指针未初始化 → 解决方案:确保 BFS 包含所有非根节点。
匹配时未回溯失败指针 :仅检查当前节点,遗漏后缀匹配的模式串 → 解决方案:匹配时通过 temp = cur 回溯失败指针收集所有结果。
内存溢出 :模式串过多时节点数组过大 → 解决方案:限制节点数量,或使用内存池复用节点。
7.2 最佳实践
字符集选择 :
固定小字符集(如小写字母):用数组 children[26],效率最高。
任意字符集:用 unordered_map<char, int>,灵活但略慢。
性能优化 :
小规模场景:用索引数组存储节点,简化内存管理。
大规模场景:预分配节点数组大小,避免频繁 emplace_back。
功能扩展 :
统计次数:新增 count 属性,匹配时累加。
去重匹配:用 unordered_set 存储已匹配的模式串,避免重复。
八、总结 AC 自动机是多模式匹配的'终极方案',核心特性可总结为:
结构核心 :Trie + 失败指针(借鉴 KMP 的部分匹配表),实现失配后快速回退。
时间复杂度 :预处理 O(∑len),匹配 O(n),与模式串数量无关。
应用场景 :敏感词过滤、日志分析、多模式串匹配等,是处理海量文本关键词的首选算法。
理解失败指针的构建逻辑(BFS + 回溯),这是 AC 自动机的灵魂。
区分'前缀匹配'和'后缀匹配',匹配时需回溯失败指针收集所有结果。
根据字符集选择合适的子节点存储方式,平衡效率与灵活性。
AC 自动机是 C++ 开发者处理文本匹配问题的核心算法,结合 Trie 和 KMP 的优势,在工业界和算法竞赛中均有广泛应用。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online