不用std::string::find因时间复杂度高(O(n×m))、无法处理重叠匹配与前缀复用;AC自动机通过Trie树+失败指针+BFS构建,支持高效多模式匹配与完整子串覆盖。

为什么不用 std::string::find 做敏感词过滤
直接循环调用 find 查每个屏蔽词,时间复杂度是 O(n × m)(n 是文本长度,m 是词典总长度),遇到长文本+大词库(比如 10 万词)会明显卡顿。更麻烦的是,它无法处理“重叠匹配”和“前缀复用”,比如词典含 "ab" 和 "abc",文本为 "abc",find 可能只返回一次匹配,漏掉子串关系。
AC 自动机核心三步:构建失败指针 + 多模式匹配 + 输出优化
AC 自动机本质是 Trie 树 + BFS 构建的失败指针(fail),让匹配失败时能快速跳转到最长可匹配后缀节点。实际编码中,最容易出错的是 fail 指针初始化顺序和输出链(output link)的构建逻辑。
- 构建 Trie 时,每个节点存
children[256](或unordered_map),并标记is_end和id(对应哪个屏蔽词) - BFS 构建 fail:根节点子节点的
fail指向根;其余节点u的子节点v,其fail[v] = children[fail[u]][c],若不存在则回退到fail[fail[u]],直到根或找到 - 输出优化:每个节点额外存
out指针,指向最近一个真实匹配的终端节点(避免每次沿 fail 链向上遍历),可通过 BFS 时同步设置:out[u] = is_end[fail[u]] ? fail[u] : out[fail[u]]
struct Node {
Node* children[256] = {};
Node* fail = nullptr;
Node* out = nullptr; // 指向最近的终结节点
int id = -1; // 屏蔽词索引,-1 表示非终点
};
void build_ac_automaton(vector& patterns) {
// 步骤1:建Trie
root = new Node();
for (int i = 0; i < patterns.size(); ++i) {
Node* u = root;
for (char c : patterns[i]) {
if (!u->children[(unsigned char)c])
u->children[(unsigned char)c] = new Node();
u = u->children[(unsigned char)c];
}
u->id = i;
}
// 步骤2:BFS建fail和out
queuezuojiankuohaophpcnNode*youjiankuohaophpcn q;
root-youjiankuohaophpcnfail = root;
for (int c = 0; c zuojiankuohaophpcn 256; ++c) {
if (root-youjiankuohaophpcnchildren[c]) {
root-youjiankuohaophpcnchildren[c]-youjiankuohaophpcnfail = root;
root-youjiankuohaophpcnchildren[c]-youjiankuohaophpcnout = root-youjiankuohaophpcnchildren[c]-youjiankuohaophpcnid != -1 ? root-youjiankuohaophpcnchildren[c] : nullptr;
q.push(root-youjiankuohaophpcnchildren[c]);
} else {
root-youjiankuohaophpcnchildren[c] = root;
}
}
while (!q.empty()) {
Node* u = q.front(); q.pop();
for (int c = 0; c zuojiankuohaophpcn 256; ++c) {
Node* v = u-youjiankuohaophpcnchildren[c];
if (!v) continue;
Node* f = u-youjiankuohaophpcnfail;
while (f != root && !f-youjiankuohaophpcnchildren[c]) f = f-youjiankuohaophpcnfail;
v-youjiankuohaophpcnfail = f-youjiankuohaophpcnchildren[c];
v-youjiankuohaophpcnout = v-youjiankuohaophpcnfail-youjiankuohaophpcnid != -1 ? v-youjiankuohaophpcnfail : v-youjiankuohaophpcnfail-youjiankuohaophpcnout;
q.push(v);
}
}}
匹配时如何高效收集所有命中位置和词ID
单次扫描文本,每步更新当前节点,再沿 out 链收集所有匹配。注意:不能只检查当前节点 id,必须递归查 out,否则漏掉“abc”匹配时同时触发“bc”(如果词典里有)。
立即学习“C++免费学习笔记(深入)”;
- 匹配循环中,
cur = cur->children[(unsigned char)s[i]],若为空则跳到cur->fail继续找,直到成功或回到根 - 每次移动后,用临时指针
p = cur,while(p) { 记录 p->id;p = p->out; },这样能拿到所有后缀匹配项 - 若只需判断是否含屏蔽词(不关心位置),可设布尔标志 early-exit,一命中就返回 true
vector> find_all(const string& s) { // {pos, pattern_id} vector > res; Node* cur = root; for (int i = 0; i < s.size(); ++i) { unsigned char c = s[i]; while (cur != root && !cur->children[c]) cur = cur->fail; cur = cur->children[c] ? cur->children[c] : root; for (Node* p = cur; p; p = p-youjiankuohaophpcnout) { if (p-youjiankuohaophpcnid != -1) { res.emplace_back(i - patterns[p-youjiankuohaophpcnid].size() + 1, p-youjiankuohaophpcnid); } } } return res;}
内存与编码细节:中文、大小写、特殊字符怎么处理
AC 自动机本身不关心字符语义,只依赖字节值。所以 UTF-8 中文会占多个字节,直接按
unsigned char索引会崩。常见解法是预处理:把 UTF-8 字符串转成 Unicode 码点序列(如用std::wstring_convert或 C++11,但后者已弃用),再用map替代数组。更轻量的做法是统一转小写+正则清洗后匹配,或用 ICU 库做标准化。
- 大小写不敏感?在插入词典前对每个
pattern调用transform(..., ::tolower),匹配时也对输入文本做同样转换- 允许模糊匹配(如星号通配)?AC 自动机不支持,得换 Aho-Corasick + NFA 扩展,或改用正则引擎(
std::regex性能差,慎用)- 高频更新词典?每次重建 AC 自动机开销大,可考虑双层结构:热词走哈希表快速匹配,冷词走 AC,或用增量式 fail 更新(极少实用)
真正上线时,最常被忽略的是
out指针的正确性验证——它必须指向 *某个真实终结节点*,而不是任意 fail 节点。建议加单元测试:用{"a", "ab", "bc"}和文本"abc",确保返回三个匹配(位置 0/0、0/1、1/2)。











