图谱连接

草稿

确定性有限自动机(DFA)

用有限个状态和每个输入符号唯一确定的转移,识别简单的字符串模式。

concept beginner theory-of-computingautomataregular-languages

钩子问题

想象一个注册表单,只接受一个非常小的“类似邮箱”的模式:一个或多个本地字符,一个 @,一个或多个域名字符,一个 .,最后是一个或多个后缀字符。

在这个教学模型里,字母和数字算作 char@ 算作 at. 算作 dot,其他符号都算作 other。本地部分不允许出现点。它不是现实中的 RFC 邮箱校验,只是用来学习有限记忆的小语言。

玩具注册字符串这个验证器看到的是符号类别,而不是真实邮件语法。
ana@cs.ai

被玩具模式接受

acharin-localncharin-localacharin-local@atneed-domainccharin-domainscharin-domain.dotneed-suffixacharin-suffixicharin-suffix
ana@.ai

拒绝:点出现太早

acharin-localncharin-localacharin-local@atneed-domain.dotdeadachardeadichardead

第一个朴素想法

第一次扫描时,可以用几个标志位(flag):seenAtseenDothasLocalhasDomainhasSuffix。这种代码可以写对,所以它是一个公平的起点。

痛点在于:正确性藏在布尔组合里。hasLocal && seenAt && !hasDomain 的意思是“下一个有效符号必须是域名字符”。hasLocal && seenAt && hasDomain && seenDot && hasSuffix 的意思是“如果输入现在结束,就接受”。标志位并不错误,只是这些含义没有名字。

标志位隐藏含义有些标志位组合是有用的,但含义藏在布尔值里。
有用标志位组合到命名状态的映射
布尔组合命名状态显式含义
hasLocal && !seenAtin-local已有本地部分;现在允许 @
hasLocal && seenAt && !hasDomainneed-domain下一个有效符号必须是域名字符
hasLocal && seenAt && hasDomain && !seenDotin-domain域名字符正在读取;现在允许一个点
hasLocal && seenAt && hasDomain && seenDot && !hasSuffixneed-suffix点已经出现;后缀必须开始
hasLocal && seenAt && hasDomain && seenDot && hasSuffixin-suffix若输入现在结束则接受

它在哪里变脆

几个小错误会暴露隐藏情况。ana@.ai 已经有本地部分和 @,但在任何域名字符之前读到了 dotana@@cs.ai 读到了第二个 @ana@cs. 到达了点,却没有后缀字符。

确定性有限自动机(deterministic finite automaton, DFA)会把每个脆弱情况显式写出来:读完 ana@ 后,机器处于 need-domain;如果此时读到 dot,唯一的下一状态就是 dead

缺少域名字符`ana@.ai` 正是在 `need-domain` 读到点时失败。
acharin-localncharin-localacharin-local@atneed-domain.dotdeadachardeadichardead
确定性轨迹
前缀读取符号状态含义
ε--need-local还没有读到有效内容;下一个符号必须是字母或数字。
aacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anncharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anaacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
ana@@atneed-domain@ 已经出现;下一个符号必须是第一个域名字符。
ana@..dotdead已经发生致命格式错误;后续任何符号都会保持拒绝。
ana@.aachardead已经发生致命格式错误;后续任何符号都会保持拒绝。
ana@.aiichardead已经发生致命格式错误;后续任何符号都会保持拒绝。

核心发明

DFA 把分散的标志位替换成一个当前状态(state)。状态是对“已经读过的前缀”的命名摘要:哪些事实还重要,接下来还必须发生什么。

在这个玩具验证器里,need-local 表示“还没有本地字符”。in-domain 表示“已经有本地部分、恰好一个 @、并且域名已经开始,所以现在允许点”。dead 表示已经发生致命错误,这个输入无法修复。

状态含义每个命名状态都说明接下来还必须发生什么。
need-local需要本地字符

还没有读到有效内容;下一个符号必须是字母或数字。

in-local本地部分中

已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。

need-domain需要域名字符

@ 已经出现;下一个符号必须是第一个域名字符。

in-domain域名部分中

已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。

need-suffix需要后缀字符

域名中的点已经出现;下一个符号必须是第一个后缀字符。

in-suffix后缀部分中

已经有至少一个后缀字符;只有输入在这里结束时才接受。

dead死状态 / 陷阱状态

已经发生致命格式错误;后续任何符号都会保持拒绝。

可视化模型

整台机器可以写成一张表:每个状态对每一类输入符号,都恰好有一个下一状态。这就是“确定性”的含义。

例如,δ(in-local,@)=need-domain\delta(\text{in-local}, @) = \text{need-domain},而 δ(in-local,dot)=dead\delta(\text{in-local}, \text{dot}) = \text{dead}。这张转移表是完整的:没有哪个情况被留给隐式默认值。

七个状态乘四类符号每个状态对每类输入都恰好有一个下一状态。
char@chardotcharchardot/other@/dot/otherneed-local需要本地字符need-local: 还没有读到有效内容;下一个符号必须是字母或数字。in-local本地部分中in-local: 已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。need-domain需要域名字符need-domain: @ 已经出现;下一个符号必须是第一个域名字符。in-domain域名部分中in-domain: 已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。need-suffix需要后缀字符need-suffix: 域名中的点已经出现;下一个符号必须是第一个后缀字符。in-suffix后缀部分中in-suffix: 已经有至少一个后缀字符;只有输入在这里结束时才接受。dead死状态 / 陷阱状态dead: 已经发生致命格式错误;后续任何符号都会保持拒绝。
完整转移表
状态charatdotother
need-localin-localdeaddeaddead
in-localin-localneed-domaindeaddead
need-domainin-domaindeaddeaddead
in-domainin-domaindeadneed-suffixdead
need-suffixin-suffixdeaddeaddead
in-suffixin-suffixdeaddeaddead
deaddeaddeaddeaddead

互动演示

试着逐步运行 ana@cs.aiana@.aiana@cs.ai.。请同时看文字状态和颜色:处于 in-suffix 只说明“如果输入现在结束就接受”。

当前状态

未定:必须读完整个输入后才判断接受。

当前状态
need-local (需要本地字符)
已读前缀
ε
剩余输入
ana@cs.ai
下一个符号
a -> char

在读取输入前开始。机器仍需要第一个本地字符。

char@chardotcharchardot/other@/dot/other*need-local需要本地字符in-local本地部分中need-domain需要域名字符in-domain域名部分中need-suffix需要后缀字符in-suffix后缀部分中dead死状态 / 陷阱状态
achar-nchar-achar-@at-cchar-schar-.dot-achar-ichar-
轨迹账本
当前前缀读取符号状态状态说明
当前ε--need-local还没有读到有效内容;下一个符号必须是字母或数字。
aacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anncharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anaacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
ana@@atneed-domain@ 已经出现;下一个符号必须是第一个域名字符。
ana@cccharin-domain已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。
ana@csscharin-domain已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。
ana@cs..dotneed-suffix域名中的点已经出现;下一个符号必须是第一个后缀字符。
ana@cs.aacharin-suffix已经有至少一个后缀字符;只有输入在这里结束时才接受。
ana@cs.aiicharin-suffix已经有至少一个后缀字符;只有输入在这里结束时才接受。
模拟器使用的转移表
状态charatdotother
need-localin-localdeaddeaddead
in-localin-localneed-domaindeaddead
need-domainin-domaindeaddeaddead
in-domainin-domaindeadneed-suffixdead
need-suffixin-suffixdeaddeaddead
in-suffixin-suffixdeaddeaddead
deaddeaddeaddeaddead

形式化版本

有了轨迹直觉之后,数学对象很紧凑:

M=(Q,Σ,δ,q0,F)M = (Q, \Sigma, \delta, q_0, F)

这里 Q 是有限状态集合,Σ 是有限输入字母表,δ 是转移函数(transition function),q0 是起始状态,F 是接受状态集合。

在本节点里,Q 是七个命名状态,Σ = { char, at, dot, other }q0 = need-localF = { in-suffix }。函数 δ 正是上面的转移表。

具体的五元组图变成状态集合、字母表、转移函数、起始状态和接受集合。
Q

need-local, in-local, need-domain, in-domain, need-suffix, in-suffix, dead

Σ

char, at, dot, other

δ

上面的转移表

q0

need-local

F

{ in-suffix }

实现草图

实现方式和可见轨迹一一对应:分类一个字符,查询一个表格单元,替换当前状态,最后在循环结束后检查是否接受。

type DfaSymbol = "char" | "at" | "dot" | "other";
type DfaState =
  | "need-local"
  | "in-local"
  | "need-domain"
  | "in-domain"
  | "need-suffix"
  | "in-suffix"
  | "dead";

function classify(input: string): DfaSymbol {
  if (/^[A-Za-z0-9]$/.test(input)) return "char";
  if (input === "@") return "at";
  if (input === ".") return "dot";
  return "other";
}

function accepts(input: string) {
  let state: DfaState = "need-local";
  for (const char of input) {
    const symbol = classify(char);
    state = transitionTable[state][symbol];
  }
  return state === "in-suffix";
}
代码到轨迹的映射分类器、查表和最终接受检查对应可见轨迹。
1

分类一个输入字符

2

查询 transition[state][symbol]

3

替换当前状态

end

仅当最终状态在 F 中才接受

前缀不变量

每读完一个前缀,当前状态都会概括后续计算需要知道的一切。它不记住字面字符串 ana@cs,而是记住相关阶段:本地部分已经完成,@ 已经完成,域名已经开始,点还没有出现。

核心不变量是:如果两个前缀落在同一个状态,那么接上任何相同的剩余后缀,它们都会得到相同的接受/拒绝答案。

前缀账本每个前缀读完后,一个状态概括所有仍然重要的信息。
确定性轨迹
前缀读取符号状态含义
ε--need-local还没有读到有效内容;下一个符号必须是字母或数字。
aacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anncharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anaacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
ana@@atneed-domain@ 已经出现;下一个符号必须是第一个域名字符。
ana@cccharin-domain已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。
ana@csscharin-domain已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。
ana@cs..dotneed-suffix域名中的点已经出现;下一个符号必须是第一个后缀字符。
ana@cs.aacharin-suffix已经有至少一个后缀字符;只有输入在这里结束时才接受。
ana@cs.aiicharin-suffix已经有至少一个后缀字符;只有输入在这里结束时才接受。

复杂度

对于长度为 n 的输入,DFA 每个字符执行一次转移,所以运行时间是 O(n)O(n)。转移表有 QΣ|Q||\Sigma| 个条目;这里是 7 * 4。除了输入本身,工作记忆是 O(1)O(1),因为机器只保存当前状态。

一个符号,一次查表运行 DFA 对输入长度是线性的,并且只用常数额外记忆。
输入长度n转移查询nO(n)
n输入符号

长度为 n 的输入从左到右读取,每次读取一个符号。

n次转移

每个符号触发恰好一次转移查询,所以时间是 O(n)。

7 * 4个表格条目

有限表格有 |Q||Σ| 个单元:七个状态乘四类符号。

1个当前状态变量

运行时除了输入之外只保存当前状态,所以工作记忆是 O(1)。

常见误解

接受是根据最终状态判断的。运行 ana@cs.ai. 时,前缀 ana@cs.ai 会到达 in-suffix,当时它是接受状态。但输入还剩一个 .。机器必须继续读取它,而从 in-suffix 读到 dot 会进入 dead,所以整个字符串被拒绝。

只在末尾判断接受`ana@cs.ai.` 曾经过接受状态,但最后一个点把它送到死状态。
acharin-localncharin-localacharin-local@atneed-domainccharin-domainscharin-domain.dotneed-suffixacharin-suffixicharin-suffix.dotdead

读完前缀 ana@cs.ai 后状态是 in-suffix,但机器仍必须读取最后的点。最后一次转移决定整个字符串。

确定性轨迹
前缀读取符号状态含义
ε--need-local还没有读到有效内容;下一个符号必须是字母或数字。
aacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anncharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
anaacharin-local已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。
ana@@atneed-domain@ 已经出现;下一个符号必须是第一个域名字符。
ana@cccharin-domain已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。
ana@csscharin-domain已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。
ana@cs..dotneed-suffix域名中的点已经出现;下一个符号必须是第一个后缀字符。
ana@cs.aacharin-suffix已经有至少一个后缀字符;只有输入在这里结束时才接受。
ana@cs.aiicharin-suffix已经有至少一个后缀字符;只有输入在这里结束时才接受。
ana@cs.ai..dotdead已经发生致命格式错误;后续任何符号都会保持拒绝。

死状态不是惩罚,而是记忆捷径。一旦发生致命错误,剩下的输入无法让这个玩具模式重新合法,所以每类符号都会自环回到 dead

死状态自环发生致命错误后,剩余输入无法修复这个玩具模式。
char, @, dot, otherneed-local需要本地字符need-local: 还没有读到有效内容;下一个符号必须是字母或数字。in-local本地部分中in-local: 已经有至少一个本地字符;后面可以继续本地字符,或出现一个 @。need-domain需要域名字符need-domain: @ 已经出现;下一个符号必须是第一个域名字符。in-domain域名部分中in-domain: 已经有至少一个域名字符;后面可以继续域名字符,或出现一个点。need-suffix需要后缀字符need-suffix: 域名中的点已经出现;下一个符号必须是第一个后缀字符。in-suffix后缀部分中in-suffix: 已经有至少一个后缀字符;只有输入在这里结束时才接受。dead死状态 / 陷阱状态dead: 已经发生致命格式错误;后续任何符号都会保持拒绝。
完整转移表
状态charatdotother
need-localin-localdeaddeaddead
in-localin-localneed-domaindeaddead
need-domainin-domaindeaddeaddead
in-domainin-domaindeadneed-suffixdead
need-suffixin-suffixdeaddeaddead
in-suffixin-suffixdeaddeaddead
deaddeaddeaddeaddead

图谱连接

图的基础和 DFA 图都使用圆点与箭头,但它们建模的对象不同。普通图的边可以表示对象之间的任意关系。DFA 的箭头是读取输入时,在有限记忆状态之间发生的带标签转移。

后续节点可以从这里继续到 DFA 设计、正则语言、NFA 和抽水引理的边界。本页只聚焦确定性有限记忆和按输入生成轨迹的接受语义。

练习

  1. 在查看轨迹前,预测 a@b.c 的最终状态。
  2. 为什么 ana.@cs.ai 会在看到 @ 之前就进入 dead
  3. 哪个表格条目拒绝了 ana@cs!
  4. 如果允许本地部分出现点,需要修改什么?
预测练习用固定轨迹预测下一状态和最终结果。
a@b.c

最终状态会接受吗?

显示答案

accept

ana.@cs.ai

本地部分的点之后会出现哪个状态?

显示答案

dead

ana@cs!

哪个表格行拒绝感叹号?

显示答案

δ(in-domain, other) = dead,即 in-domain 行的 other 列

图谱连接 : 确定性有限自动机