跳至主要內容

有限自动机(Finite Automaton)

西风逍遥游大约 6 分钟

有限自动机(Finite Automaton)

有限自动机可以看成一种特殊的有向图:顶点是状态,边上标着输入符号;机器始终处在某一个状态,读入一个符号就沿对应边跳到下一状态。它和一般的 相比,多了「按输入驱动转移」的执行语义,因此特别适合做模式识别字符串扫描

编译器词法分析里大量使用自动机;更偏形式语言的讨论见 自动机理论。本章站在数据结构视角:弄清自动机是什么,以及如何用它快速匹配字符串

试一试状态转移图与转移表:

基本构成

一台(确定)有限自动机通常记为五元组 (Q,Σ,δ,q0,F)(Q, \Sigma, \delta, q_0, F)

符号含义
QQ有限的状态集合
Σ\Sigma字母表(可出现的输入字符集合)
δ:Q×ΣQ\delta: Q \times \Sigma \to Q转移函数:当前状态 + 读入字符 → 下一状态
q0Qq_0 \in Q初始状态
FQF \subseteq Q接受状态(终结态)集合

确定性(DFA):每个状态对每个字符至多(通常恰好)有一条出边,下一步唯一。
非确定性(NFA):同一状态读同一字符可能有多个去向,还可能有 ε\varepsilon 转移。NFA 与 DFA 识别能力等价,但 DFA 跑起来更干脆——读一个字符走一步,无需回溯或维护状态集合。

匹配一段输入 w=c1c2cnw = c_1 c_2 \ldots c_n 时,从 q0q_0 出发依次转移:

qi=δ(qi1,ci) q_{i} = \delta(q_{i-1}, c_i)

若最终(或中途,取决于用法)落在 FF 中的某个状态,则称自动机接受该串(或在该位置报告一次匹配)。整段扫描只做 nn 次转移,时间 O(n)O(n),与模式有多复杂无关——复杂都摊在预处理构造 δ\delta 上。

用自动机做字符串匹配

朴素匹配:模式 PP 长度 mm,文本 TT 长度 nn,每个对齐位置比较,最坏 O(nm)O(nm)
自动机方法:先把「已经匹配了模式的多长前缀」编码成状态,再对 TT 只扫一遍,总时间 O(n+预处理)O(n + \text{预处理})

状态含义

对模式 P=p0p1pm1P = p_0 p_1 \ldots p_{m-1}(下标从 0),构造一台匹配自动机,状态取

Q={0,1,2,,m} Q = \{0, 1, 2, \ldots, m\}

其中状态 kk 表示:当前已匹配上 PP 的长度为 kk 的前缀(即刚读过的一段后缀等于 P[0..k)P[0..k))。特别地:

  • 状态 00:什么都还没匹配上;
  • 状态 mm:整个模式已匹配成功,是接受状态(可设 F={m}F = \{m\};若要找所有出现位置,到达 mm 时报告一次,再按转移继续扫)。

转移函数在算什么

读入字符 cc 时,若当前在状态 kk,理想情况是:c=pkc = p_k,则转到 k+1k+1
若对不上,不能简单退回 00——文本里可能已经拼出了模式的某个真前缀。正确做法是:

看「当前已匹配的前缀 P[0..k)P[0..k) 再接上 cc」得到的串,它的最长后缀若同时也是 PP 的前缀,长度是多少,就转到那个长度对应的状态。

形式化:设 σ(k,c)\sigma(k, c) 表示串 P[0..k)+cP[0..k) + c 的所有后缀里,同时也是 PP 前缀的最大长度,则

δ(k,c)=σ(k,c) \delta(k, c) = \sigma(k, c)

对接受状态 mm 也一样:匹配成功后再读字符,相当于从「已匹配完整个 PP」继续找下一个可能的重叠出现。

例子:P=ababP = \texttt{abab}

状态 0..40..4。字母表里我们关心 ab(其它字符都回到合适的前缀长度,往往是 0)。

状态 kk含义(已匹配前缀)ab
0ε\varepsilon1(对齐到 a0
1a1(仍是前缀 a2(ab
2ab3(aba0
3aba14(abab,接受)
4abab3(后缀 ab+aaba0

在文本 aababab 上跑:

文本:  a a b a b a b
状态: 0→1→1→2→3→4→3→4
                    ↑     ↑
                 报告匹配  再次匹配

第二次到达状态 4 时,利用了模式的自重叠(abab 的后缀 ab 又是前缀),无需把窗口整段拖回去重比。

如何构造转移表

暴力:对每个状态 kk、每个字符 cc,显式取出串 P[0..k)+cP[0..k)+c,再试最长前缀长度,预处理约 O(m2Σ)O(m^2|\Sigma|),对短模式够用。

更常见的是借助失配函数(与 KMP 的 next / prefix 函数同一思想)加速构造,使预处理降到大约 O(mΣ)O(m|\Sigma|)

  1. 先算数组 π[1..m)\pi[1..m)π[q]\pi[q] = 「P[0..q)P[0..q) 的真后缀中,同时也是 PP 前缀」的最长长度。
    π[0]\pi[0] 无定义或不用;计算方式与 KMP 相同:前后指针滑动。)
  2. k=0..mk = 0..mcΣc \in \Sigma
    • k<mk < mc=pkc = p_k,则 δ(k,c)=k+1\delta(k,c) = k+1
    • 否则若 k=0k = 0,则 δ(0,c)=0\delta(0,c) = 0(或仅当 c=p0c=p_0 时为 1,上面已覆盖);
    • 否则 δ(k,c)=δ(π[k],c)\delta(k,c) = \delta(\pi[k], c)——失配后跳到次长前缀状态,再问这个字符该怎么走(可用递推/缓存避免重复算)。

直觉:失配时「已经读过的那一段」里仍可能藏着更短的合法前缀,π\pi 指出该退到哪里,不必从头比。

伪代码(扫描阶段):

// 预处理得到 δ[0..m][Σ]
q = 0
for i = 0 .. n-1:
    q = δ[q][T[i]]
    if q == m:
        报告:模式在 i-m+1 处出现

扫描严格 O(n)O(n) 次转移;每次转移 O(1)O(1)(转移表)或 O(Σ)O(|\Sigma|) 量级可忽略。整体 O(n+mΣ)O(n + m|\Sigma|) 量级,远好于朴素的 O(nm)O(nm)

和 KMP、AC 自动机的关系

方法解决什么与自动机的关系
匹配自动机(上文)单模式在文本中的所有出现显式构造 δ\delta,按 DFA 跑文本
KMP同上不存完整 δ\delta,运行时用 π\pi 在失配时跳转,本质是同一台自动机的压缩实现
Aho–Corasick多模式同时匹配把多模式做成一棵 Trie,再加失败指针,等价于一台更大的自动机;扫描仍 O(n+命中次数)O(n + \text{命中次数})

多模式场景(敏感词过滤、多关键字检索):先把所有模式插入 Trie,再 BFS 上计算每个结点的失败指针(指向「当前前缀的最长真后缀」对应结点),扫描文本时沿 Trie 走,失配则跳失败指针——这就是常说的 AC 自动机。单模式时它退化成与 KMP / 匹配自动机同类的行为。

小结

  1. 有限自动机 = 带输入标签的有向图 + 起止状态;DFA 读入线性扫描,匹配本身是 O(n)O(n)
  2. 字符串快速匹配的关键:用状态记住「模式前缀匹配了多长」,转移时处理失配与自重叠。
  3. 预处理建表(或 KMP 的 π\pi),扫描一遍文本即可找出所有出现;多模式用 AC 自动机。
  4. 词法分析里「正则 → NFA → DFA」是同一套工具换了构造来源;数据结构这边先掌握「模式 → 匹配自动机 → 线性扫描」即可。

相关阅读: · 自动机理论 · 构建自动机