多模式字符串匹配:Trie 与 AC 自动机

上一篇介绍了 BF、RK、KMP、BM,它们主要解决的是单模式字符串匹配

但实际开发中,我们经常会遇到另一类问题:

给你一段文本,同时匹配大量关键词。

例如:

复制代码
文本:
我喜欢 Java、Go 和 Python,也在学习 Rust。

关键词:
Java
Go
Python
Rust
C++

最直接的做法,是拿每一个关键词分别去匹配文本。

但如果关键词有几万、几十万个,这种方式效率就比较低了。

有没有办法:

只扫描一次文本,同时匹配多个关键词?

这就是多模式字符串匹配

常见的数据结构和算法就是:

复制代码
Trie
 ↓
Trie + Failure 指针
 ↓
AC 自动机

1. Trie:先把关键词组织起来

Trie,也叫字典树

它的核心思想很简单:

把多个字符串按照公共前缀组织起来。

例如有三个关键词:

bash 复制代码
cat
car
dog

构建 Trie:

yaml 复制代码
       root
      /    \
     c      d
     |      |
     a      o
    / \     |
   t   r    g

catcar 都有:

复制代码
ca

这个公共前缀,所以可以复用节点。

相比直接保存字符串,Trie 可以把公共前缀压缩掉。

Trie 特别适合:

  • 前缀搜索
  • 自动补全
  • 字典查询
  • IP 路由
  • 关键词匹配

但是,仅仅有 Trie 还不够。


2. Trie 的问题:匹配失败怎么办?

假设关键词:

复制代码
he
she
his
hers

我们构建 Trie 后开始匹配文本:

复制代码
ushers

当匹配到:

复制代码
she

如果后面继续匹配失败了,怎么办?

最简单的想法:

复制代码
匹配失败
   ↓
回到 root
   ↓
重新匹配

这样虽然能够完成匹配,但是会产生大量重复操作。

更重要的是:

当前已经匹配到的字符,其实可能还有利用价值。

例如:

复制代码
she

它的后缀:

复制代码
he

本身就是一个关键词。

所以匹配失败之后,我们没必要全部回退。

能不能直接跳到:

复制代码
he

继续匹配?

可以。

这就是 Failure 指针


3. AC 自动机 = Trie + Failure 指针

AC 自动机可以简单理解成:

复制代码
AC 自动机 = Trie + Failure 指针

其中:

Trie 负责什么?

Trie 负责:

保存多个模式串,并表示字符之间的匹配关系。

Failure 指针负责什么?

Failure 指针负责:

匹配失败时,告诉我们应该跳到哪里继续匹配。

这个思想其实和 KMP 非常像。

KMP:

lua 复制代码
一个模式串
     ↓
next 数组
     ↓
匹配失败
     ↓
跳到合适的位置

AC 自动机:

markdown 复制代码
多个模式串
     ↓
Trie
     ↓
Failure 指针
     ↓
匹配失败
     ↓
跳到合适的节点

所以可以把 AC 自动机理解成:

把 KMP 的失败跳转思想扩展到了 Trie 上。


4. Failure 指针到底是什么?

Failure 指针是 AC 自动机最核心的部分。

简单来说:

一个节点的 Failure 指针,指向当前字符串的最长可匹配后缀

还是看:

复制代码
she

它的后缀有:

复制代码
he
e

如果:

复制代码
he

也是 Trie 中存在的路径,那么:

复制代码
she
 ↓
failure
 ↓
he

这样当当前路径无法继续匹配时,就可以跳到 he

也就是说:

markdown 复制代码
当前匹配失败
      ↓
不要从 root 重新开始
      ↓
找到最长可复用后缀
      ↓
继续匹配

这就是 Failure 指针的价值。


5. Failure 指针和 KMP 的关系

如果你已经理解 KMP,那么理解 AC 会容易很多。

KMP 解决的是:

diff 复制代码
一个字符串
+
一个模式串

它通过 next 数组保存:

当前匹配失败之后,最多可以保留多少已经匹配的信息。

AC 自动机解决的是:

diff 复制代码
一个字符串
+
很多模式串

它通过 Trie 保存所有模式串,再通过 Failure 指针保存:

当前节点匹配失败后,可以跳到哪个节点继续匹配。

可以简单记成:

lua 复制代码
KMP
  ↓
next
  ↓
单模式串失败跳转

AC
  ↓
Trie + Failure
  ↓
多模式串失败跳转

所以从思想上看,AC 自动机并不是凭空出现的新东西。

它实际上是在:

Trie 的基础上加入类似 KMP 的失败跳转思想。


6. AC 自动机是怎么构建 Failure 指针的?

Failure 指针通常使用 BFS 来构建。

为什么使用 BFS?

因为一个节点的 Failure 指针,依赖于它的父节点 Failure 指针。

例如:

css 复制代码
root
 ↓
a
 ↓
b
 ↓
c

计算 c 的 Failure 时,需要先知道:

css 复制代码
b.failure

所以需要按照:

复制代码
root
 ↓
第一层
 ↓
第二层
 ↓
第三层

这样的顺序处理。

因此一般使用队列:

markdown 复制代码
root 入队

while queue 不为空:

    取出一个节点

    遍历它的子节点

    计算子节点的 failure

    子节点入队

这也是为什么 AC 自动机的 Failure 构建通常会使用 BFS。


7. 一个简单的匹配过程

假设关键词:

复制代码
he
she
his
hers

文本:

复制代码
ushers

Trie 中已经建立好:

markdown 复制代码
h
├── e
│   └── r
│       └── s
└── i
    └── s

s
└── h
    └── e

扫描:

复制代码
u s h e r s

首先:

复制代码
u

没有匹配路径。

继续:

复制代码
s → h → e

匹配到了:

复制代码
she

同时,由于:

复制代码
she

的 Failure 可以指向:

复制代码
he

所以还可以识别:

复制代码
he

继续向后:

复制代码
r → s

最终还可以匹配:

复制代码
hers

一次扫描,就可以得到多个匹配结果。


8. 为什么 AC 自动机适合大量关键词?

假设现在有:

复制代码
10 万个关键词

文本长度:

yaml 复制代码
1000 万字符

如果每个关键词都单独进行匹配,就需要反复扫描文本。

而 AC 自动机可以:

markdown 复制代码
所有关键词
    ↓
一次构建 Trie
    ↓
建立 Failure
    ↓
扫描文本
    ↓
一次完成多关键词匹配

这也是 AC 自动机最大的优势:

关键词很多,但文本只需要扫描一遍。


9. 时间复杂度

假设:

ini 复制代码
n = 文本长度

m = 所有模式串的总长度

z = 最终匹配结果数量

那么:

scss 复制代码
Trie 构建:

O(m)

Failure 指针构建通常可以做到:

scss 复制代码
O(m)

文本匹配:

scss 复制代码
O(n + z)

因此整体可以理解为:

scss 复制代码
构建:
O(m)

匹配:
O(n + z)

其中 z 是输出结果数量。

为什么需要加 z

因为如果文本中真的出现了大量匹配结果,那么算法至少需要把这些结果输出出来。

例如:

erlang 复制代码
关键词:
a
aa
aaa
aaaa
...

文本:
aaaaaaaaaa...

可能产生大量匹配结果。

所以不能简单地说匹配永远严格是 O(n),更准确的是:

scss 复制代码
O(n + z)

10. AC 自动机适合哪些场景?

AC 自动机的典型应用就是:

敏感词过滤

例如:

复制代码
关键词:
赌博
诈骗
色情
广告

对一篇文章进行一次扫描,就可以找到所有关键词。

搜索系统

例如:

复制代码
关键词:
Java
Go
Python
Rust
C++

对文章内容进行多关键词匹配。

日志分析

例如监控日志:

bash 复制代码
ERROR
WARN
timeout
connection refused
OOM

一次扫描日志,就可以找到多个异常关键词。

内容审核

例如:

复制代码
违规词
品牌词
广告词
敏感词

都可以放进同一个 AC 自动机。


11. Trie、KMP、AC 到底是什么关系?

可以用一张图记住:

markdown 复制代码
字符串匹配
│
├── 单模式匹配
│   ├── BF
│   ├── RK
│   ├── KMP
│   └── BM
│
└── 多模式匹配
    │
    └── Trie
         │
         └── Failure 指针
              │
              └── AC 自动机

其中:

复制代码
Trie

解决的是:

如何组织大量模式串?

而:

复制代码
Failure

解决的是:

匹配失败之后如何快速跳转?

最终:

复制代码
Trie + Failure

形成:

复制代码
AC 自动机

实现多模式字符串匹配。


总结

如果只记住几个关键词:

复制代码
Trie
  ↓
多个字符串共享前缀

Failure
  ↓
匹配失败快速跳转

AC 自动机
  ↓
Trie + Failure
  ↓
一次扫描匹配多个关键词

再和 KMP 联系起来:

lua 复制代码
KMP:
单模式 + next

AC:
多模式 + Trie + Failure

一句话总结:

AC 自动机本质上就是把 Trie 和 KMP 的失败跳转思想结合起来,让我们能够在一次文本扫描中高效匹配大量关键词。

相关推荐
木井巳1 小时前
【BFS/DFS 解决 FloodFill 算法】衣橱整理
java·算法·leetcode·深度优先·广度优先·宽度优先
热心网友俣先生1 小时前
2026年高教社杯数模国赛A题:五年命题规律与预测
算法·机器学习·数学建模
嘻哈baby1 小时前
不用 LangChain,从 0 写一个 Ollama 本地 RAG
后端
考虑考虑2 小时前
Redis8.8新特性
运维·redis·后端
geovindu2 小时前
java:Observer Pattern
java·开发语言·后端·观察者模式·设计模式·行为模式
LuminousCPP2 小时前
数据结构 - 排序(二):快速排序从错误初版到优化版|双指针划分 + 三数取中 + 小区间插入优化
c语言·数据结构·笔记·算法·排序算法
山甫aa2 小时前
【从零开始的 Web 后端学习】令牌技术一篇搞定(JWT 登录认证保姆级)
后端·学习·spring·web·jwt
wxwx_bscxy3222 小时前
springboot巡更系统10192
java·spring boot·后端·巡更系统
hansang_IR2 小时前
【题解】P9753 [CSP-S 2023] 消消乐
数据结构·c++·算法