草稿
非确定性有限自动机(NFA)
让有限状态机在可能的位置分支,只要有一条完整路径成功就接受输入。
钩子问题
我们想识别所有包含子串 01 的二进制字符串。010 应该被接受,因为前两个符号就是 01。1110 应该被拒绝,因为没有任何一个 0 后面紧跟 1。
这个语言本身不难,这是有意为之。本节点的重点是比较两种有限记忆故事:DFA 的单一路径,和非确定性有限自动机(nondeterministic finite automaton, NFA)的分支路径。
找到模式
没有 01
找到模式
DFA 基线
DFA 可以用一个当前状态解决这个问题。它记住“刚才是否看到一个有希望的 0”,一旦看到 01,就可以一直保持接受。
这个确定性视角完全可行。NFA 给出另一种视角:每遇到一个 0,机器既继续正常扫描,也开启一条分支(branch),把这个 0 当作 01 的可能起点。
一个当前状态记住前一个符号是否是有希望的 0。
当前:刚看到 0同一个前缀可以保留 q0,同时尝试 q1 作为候选起点。
分支想法
最小的新发明是:一次转移可以返回多个下一状态。从 q0 读到输入 0 时,机器同时到达 q0 和 q1:
一条分支表示“继续寻找”。另一条分支表示“试着把这个 0 当作 01 的开头”。
一次转移可以返回集合:delta(q0, 0) = {q0,q1}。
每个状态的含义
这个 NFA 有三个状态:
q0:继续扫描可能的起点。q1:刚选择一个0作为01的可能起点。q2:已经看到01;接受,并保持接受。
它们不是三台独立机器,而是同一个前缀读完后可能存在的三种历史。
这条分支还没有选定某个 0;它继续扫描剩余后缀。
这条分支刚把最近的 0 选作子串 01 的可能起点。
这条分支已经看到 01,所以后续任何符号都会保持接受。
分支死亡
缺失的转移有含义。从 q1 读到 1 会完成模式并进入 q2;读到 0 则没有完成 01,所以那条旧候选分支无路可走:
对于输入 00,读完第一个 0 后,活跃状态集合(active state set)是 {q0,q1}。读第二个 0 时,旧 q1 死亡;但旧 q0 转到 {q0,q1},立刻为第二个 0 生成一个新的候选。活跃集合仍是 {q0,q1},但事件账本展示了真正发生的变化。
| 旧分支 | 事件 | 结果 |
|---|---|---|
| q0 | q0 --0--> {q0,q1} | 旧 q0 留在 q0 继续扫描,同时为这个 0 生成 q1。 |
| q1 | q1 --0--> {} | q1 在 0 上没有转移,所以那条旧分支死亡。 |
之前是 {q0,q1}。之后仍是 {q0,q1}。集合看似不变,但账本显示旧 q1 死亡,同时新 q1 生成。
活跃状态集合
我们不必把每条分支都单独画出来,可以用活跃状态集合总结所有仍活着的分支。对于输入 010,精确轨迹是:
{q0} -> {q0,q1} -> {q0,q2} -> {q0,q1,q2}
读完前缀 01 的那一行,如果输入在此结束,就会接受,因为 q2 已经活跃。但整个输入只有在所有符号都消耗完后才判断接受。
| 前缀 | 读取 | 活跃状态 | 若在此结束会接受吗? | 说明 |
|---|---|---|---|---|
| ε | - | {q0} | 否 | 开始时只有 q0 活跃:扫描可能作为 01 起点的 0。 |
| 0 | 0 | {q0,q1} | 否 | 读到 0 会保留 q0,并为这个 0 开启候选 q1 分支。 |
| 01 | 1 | {q0,q2} | 是 | 某条 q1 分支读到 1 后进入接受 q2;q0 仍继续扫描。 |
| 010 | 0 | {q0,q1,q2} | 是 | 读到 0 会保留 q0,并为这个 0 开启候选 q1 分支。 |
互动演示
逐步运行 010、00 和 1010。先用活跃集合视图看紧凑摘要,再切换到分支事件视图,看每个旧分支具体产生或失去了哪些状态。
如果输入在这里结束,会拒绝。
- 活跃状态
- {q0}
- 已读前缀
- ε
- 剩余输入
- 010
- 下一个符号
- 0
开始时只有 q0 活跃:扫描可能作为 01 起点的 0。
| 当前 | 前缀 | 读取 | 活跃状态 | 若在此结束会接受吗? |
|---|---|---|---|---|
| 当前 | ε | - | {q0} | 否 |
| 0 | 0 | {q0,q1} | 否 | |
| 01 | 1 | {q0,q2} | 是 | |
| 010 | 0 | {q0,q1,q2} | 是 |
| 状态 | 0 | 1 |
|---|---|---|
| q0 | {q0,q1} | {q0} |
| q1 | {} | {q2} |
| q2 | {q2} | {q2} |
形式化版本
不带空转移的 NFA 可以写成一个五元组:
这里 Q 是有限状态集合,\Sigma 是输入字母表,q0 是起始状态,F 是接受状态集合。
它和 DFA 的关键区别在转移函数:
P(Q) 表示 Q 的幂集:所有状态子集组成的集合。一次转移返回的是一组可能的下一状态,而不是唯一的下一状态。
在这个包含 01 的 NFA 中,Q = { q0, q1, q2 },\Sigma = { 0, 1 },q0 = q0,F = { q2 },转移表如下图所示。
{ q0, q1, q2 }
{ 0, 1 }
Q x Σ -> P(Q)
q0
{ q2 }
| 状态 | 0 | 1 |
|---|---|---|
| q0 | {q0,q1} | {q0} |
| q1 | {} | {q2} |
| q2 | {q2} | {q2} |
空转移说明
有些 NFA 定义还允许空转移(epsilon transition)。空转移写作 ,意思是不消耗输入也能移动。
本节点不使用空转移。主体实现保持无 epsilon 的形式:。带 epsilon 的 NFA 会把转移域扩展到包含 ,并需要 epsilon-closure 之类的闭包记录;这是有用的后续主题,但不是理解这里的分支语义所必需的。
空转移(epsilon transition)不消耗输入。本节点的主体机器没有空转移,所以 delta 保持 Q x Sigma -> P(Q)。
实现草图
直接模拟时,程序保存一组活跃状态。对每个输入符号,询问每个当前活跃状态能去哪里,然后把所有答案取并集:
对于 010,读完前缀 0 后,S_1 = {q0,q1}。下一个符号是 1,所以:
function accepts(input: string) {
let activeStates = new Set<NfaState>(["q0"]);
for (const symbol of input) {
const nextActiveStates = new Set<NfaState>();
for (const state of activeStates) {
for (const next of transitionTable[state][symbol]) {
nextActiveStates.add(next);
}
}
activeStates = nextActiveStates;
}
return activeStates.has("q2");
}
let next = new Set<NfaState>();
for (const q of activeStates) {
for (const r of delta[q][symbol]) {
next.add(r);
}
}从 S1 = {q0,q1} 读 1:
{q0} union {q2} = {q0,q2}
前缀不变量
消耗前 i 个符号后,S_i 正好包含所有“读完这个前缀后可由某条分支到达”的状态。程序不会在之后凭空发明分支,也不会忘记仍活着的分支。
这个不变量解释了接受规则:
换成话说:整个输入消耗完后,只要至少一条活跃分支处于接受状态,就接受。
读完前缀 01 后,S2 = {q0,q2}。因为 q2 活跃,若输入在这里结束就会接受。
复杂度
对于长度为 n 的输入,直接模拟会在每个符号上扫描当前活跃状态。活跃状态最多有 |Q| 个,所以在查表表示下,运行时间是 。转移表有 |Q||Σ| 个集合值单元。
后续的 subset construction 节点会说明如何把活跃集合编译成 DFA 状态。本页只讨论直接模拟 NFA。
输入符号
每步最多扫描的活跃状态数
转移表单元
固定字母表查表下的直接模拟时间
常见误解
一条分支死亡,不等于整个输入被拒绝。对 00,旧 q1 候选死亡,但 q0 分支继续存在,所以运行仍然有效。
另外,acceptedIfInputEndedHere 和整个输入的 accepted 不是同一个字段。轨迹中的某一行只是在说“如果输入在这里结束,会接受”;最终答案只在最后一行检查。
在 00 中,旧 q1 死亡,但旧 q0 让运行继续。
轨迹行说的是若输入在此结束会接受;整个输入只在最后一行判断。
图谱连接
DFA 是前置知识:先理解“一个当前状态”的语义,再看 NFA 如何把这个想法推广成“同一个前缀可以有一组可能的当前状态”。
未来节点可以把 NFA 连接到 subset construction、正则表达式和 epsilon-NFA 构造。那些节点还不存在,所以本次不添加对应边。
练习
- 预测
01的活跃集合轨迹。 - 对
00,第二个符号上哪条旧分支死亡? - 为什么
1010虽然以1开头,最终仍然接受? - 如果允许空转移,实现需要增加什么?
预测最终活跃集合和结果。
显示答案
{q0,q2} - 接受
预测最终活跃集合和结果。
显示答案
{q0,q1} - 拒绝
预测最终活跃集合和结果。
显示答案
{q0,q1,q2} - 接受
预测最终活跃集合和结果。
显示答案
{q0} - 拒绝
图谱连接 : 非确定性有限自动机