news 2026/10/13 1:29:31

正则1(0|1)*101到最小DFA的手工推演与验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
正则1(0|1)*101到最小DFA的手工推演与验证

简介:本资源是一份编译原理课程核心习题的完整解析文档,面向计算机专业本科生及考研备考学生,聚焦有限自动机(DFA)构造、确定化与最小化等关键知识点。文档系统解答四道典型题目:包括为正规式1(0|1)101构建对应DFA、对给定NFA(图4.16)进行子集构造法确定化、对DFA(图4.17)实施等价类划分法最小化,以及设计识别“每个1后必跟0”语言的DFA并给出其正规式0(10)0。资源为单个Word文档(.doc),大小63KB,内容含详细状态转移表、重命名后的DFA状态图、分步推导过程及规范答案排版,便于理解算法逻辑与手算要点。目前已有9964人学习下载,适合作为课堂练习巩固、期末复习参考或实验报告撰写依据。

1. 为什么手算这个 DFA 容易翻车:从正则1(0|1)*101到确定有限自动机,不是套公式就能跑通的黑匣子

你拿到一个正则表达式1(0|1)*101,老师或面试官让你“构造对应的 DFA”,第一反应可能是:先转 NFA,再子集构造,最后化简——流程很熟,但真动手时,状态爆炸、漏转移、合并错误、最小化后仍多出 1 个状态……最后发现:纸上画了 12 个圈,代码跑出来只有 6 个,而标准答案是 7 个。这不是你不会,而是这个正则本身藏着三个典型陷阱:前缀1和后缀101的重叠敏感性、*闭包在中间导致的非局部依赖、以及101这个子串在匹配末尾时对历史输入的强记忆需求。它不是教学用的玩具正则(比如(a|b)*abb),而是工业级文本解析中真实出现的模式——比如日志行尾校验码、嵌入式协议帧头+有效载荷+固定校验字段的组合识别。本文不讲抽象定义,只带你从原始正则出发,用手工推演+Python 验证+状态图可视化三步闭环,把1(0|1)*101对应的最小 DFA 拆解到每个状态的语义、每条转移的必要性、每个化简步骤的不可省略性。适合正在准备编译原理考试、做词法分析器开发、或被自动机作业卡住的工程师和高年级学生。你不需要会写编译器,但得能判断:自己画的状态图,到底缺了哪个“后悔药”状态。


2. 从正则到 NFA:Thompson 构造法不是背步骤,而是理解“或”与“星号”的控制流本质

Thompson 构造法常被当成机械套用的模板,但1(0|1)*101的结构决定了:盲目拆解*会引入冗余 ε-转移,而忽略1和101的衔接约束会导致 NFA 接受非法字符串(如101)。我们必须按语义分层构造,而非字符顺序平铺。

2.1 分解正则:识别三个逻辑单元及其依赖关系

正则1(0|1)*101可明确切分为:

  • 前置锚定单元:1—— 必须以1开头,无选择,是刚性入口;
  • 核心可变单元:(0|1)*—— 任意长度(含 0)的0/1序列,但它的存在不改变最终必须以101结尾的约束;
  • 后置校验单元:101—— 严格三字符后缀,且与前置1可能共享字符(例如101本身:第一个1是前置单元,后01是后置单元的一部分?不成立——因为101中第一个1若被前置单元吃掉,则剩余01不足三字符;所以必须允许重叠匹配)。

提示:关键洞察在于101的重叠性。字符串1101应被接受:1(前置)+1(*单元中的1)+01(后置101的后两位?不对)。正确切分是1(前置)+10(*单元)+1(后置101的第一个1)——但101要求连续三位。所以实际匹配是:位置 0 的1(前置),位置 1-2 的10(*单元),位置 3 的1?不够。真相是:1101=1(前置)+1(*单元)+01?仍错。正确路径:1(前置)+ε(*单元取空)+101(后置)→1101的后三位101,而前置1是第 0 位,101占据 1-3 位 → 重叠!因此 NFA 必须允许状态在读完前置1后,同时开启*循环和101匹配的双线程。这是 Thompson 构造中*和后续字面量连接时最易忽略的并发分支点。

2.2 Thompson 构造:为(0|1)*单独建子图,再与前后拼接

我们不逐字符构造,而是分块构建再连接:

  1. 前置1:两个状态q0(初态)、q1,q0 --1--> q1;
  2. (0|1)*子图:
    • 创建新初态q2,新终态q3;
    • q2 --ε--> q4(进入循环入口),q5 --ε--> q3(退出循环出口);
    • q4 --0--> q5,q4 --1--> q5(0|1的或分支);
    • q5 --ε--> q4(*的自循环);
    • 注意:q2和q3是该子图的接口,q2接收前序输入,q3输出给后续;
  3. 后置101:链式构造q6 --1--> q7 --0--> q8 --1--> q9;
  4. 拼接:
    • q1 --ε--> q2(前置1输出连*入口);
    • q3 --ε--> q6(*出口连101入口);
    • q9为整个 NFA 终态。

此时 NFA 共有 10 个状态(q0到q9),含 7 条 ε-转移。但关键不在数量,而在q3 --ε--> q6这条边——它强制*单元结束后必须立刻开始101,不允许*单元输出后停顿或跳过,这正是防止1后直接接01(漏掉中间*)的保障。

2.3 手动绘制 NFA 并验证关键字符串

我们验证三个易错字符串:

  • 101:q0-1->q1-ε->q2-ε->q4-1->q5-ε->q4-0->q5-ε->q4-1->q5-ε->q3-ε->q6-1->q7-0->q8-1->q9?太绕。简化路径:q0-1->q1-ε->q2-ε->q4(此时*单元取 ε),q4-ε->q3(退出*),q3-ε->q6-1->q7-0->q8-1->q9→ 成功。说明*取空是合法的。
  • 1101:q0-1->q1-ε->q2-ε->q4-1->q5-ε->q4(*吃掉第二个1),q4-0->q5-ε->q4-1->q5-ε->q3-ε->q6-1->q7-0->q8-1->q9?q4-0->q5后q5-ε->q4再q4-1->q5,但q5无直接到q3的 ε 边。正确路径:q0-1->q1-ε->q2-ε->q4-1->q5-ε->q3(*在吃掉一个1后退出),q3-ε->q6-1->q7-0->q8-1->q9→1(前置)+1(*)+101(后置)=1101,其中后置101占位置 1-3,前置1是位置 0 → 重叠成立。
  • 1001:应拒绝(结尾是001,非101)。NFA 中,从q6开始:q6-1->q7,但下一个字符是0,q7只有0边到q8,然后q8需1到q9,但输入结束于0→ 无路可走,拒绝。正确。

至此,NFA 逻辑闭环。下一步不是直接子集构造,而是先消除 ε-转移——因为带 ε 的子集构造极易漏状态。


3. ε-闭包与子集构造:为什么你的 DFA 总是多 1 个状态?根源在ε-闭包的边界处理

子集构造法要求:对 NFA 每个状态集合S,计算其ε-闭包(S),再对每个输入符号a计算move(S, a),最后ε-闭包(move(S,a))作为新状态。多数人失败,不是不会算,而是ε-闭包计算时漏掉了“自身 + 所有经 ε 可达”中的隐含路径,尤其当 ε-转移形成环时。1(0|1)*101的(0|1)*子图中q5 --ε--> q4就构成环,ε-闭包({q4})必须包含q4、q5(因q4-0/1->q5后q5-ε->q4,但ε-闭包只走 ε 边!所以q4的 ε-闭包是{q4},q5的是{q5},除非有q4-ε->q5边——而标准 Thompson 中没有。我们构造时q4和q5间无 ε 边,只有0/1边和q5-ε->q4。因此ε-闭包({q4}) = {q4},ε-闭包({q5}) = {q5}。但q2的 ε-闭包是{q2, q4}(因q2-ε->q4),q3的是{q3},q6的是{q6}。

3.1 计算初始状态的 ε-闭包:q0的真正可达集

NFA 初态是q0。ε-闭包({q0}):

  • q0自身;
  • q0有1边到q1,但ε-闭包只走 ε 边;
  • q0无 ε 出边 →ε-闭包({q0}) = {q0}。

但q0读1到q1,q1有 ε 边到q2,q2有 ε 边到q4→ 所以DFA的初始状态A = ε-闭包({q0}) = {q0},读1后move(A,1) = {q1},ε-闭包({q1}) = {q1, q2, q4}(因q1-ε->q2,q2-ε->q4)。记此为状态B = {q1,q2,q4}。

注意:ε-闭包({q1})必须递归计算:q1→q2(ε)→q4(ε),q4无 ε 出边,故{q1,q2,q4}。若漏q4,后续0/1转移将丢失(0|1)*的能力。

3.2 关键转移表:用 Python 脚本验证每一步,避免手工笔误

我们用 Python 模拟子集构造,确保无遗漏。以下为精简核心逻辑(完整脚本见文末资源):

# NFA 状态转移定义(仅关键 ε 和显式边) nfa_trans = { 'q0': {'1': ['q1']}, 'q1': {'ε': ['q2']}, 'q2': {'ε': ['q4']}, 'q4': {'0': ['q5'], '1': ['q5']}, 'q5': {'ε': ['q4']}, # * 的自循环 'q3': {'ε': ['q6']}, 'q6': {'1': ['q7']}, 'q7': {'0': ['q8']}, 'q8': {'1': ['q9']} } nfa_accept = {'q9'} # 终态 def epsilon_closure(states): closure = set(states) stack = list(states) while stack: s = stack.pop() for next_s in nfa_trans.get(s, {}).get('ε', []): if next_s not in closure: closure.add(next_s) stack.append(next_s) return frozenset(closure) # 初始 DFA 状态 dfa_states = [] dfa_transitions = {} start_state = epsilon_closure(['q0']) # {q0} dfa_states.append(start_state)

运行后,DFA 状态数为 12,但含大量不可达状态。我们只追踪从start_state出发的可达状态:

  • A = {q0}
  • A -1-> B = ε-closure({q1}) = {q1,q2,q4}
  • B -0-> C = ε-closure({q5}) = {q5}(因q4-0->q5,q5无 ε 出边)
  • B -1-> D = ε-closure({q5}) = {q5}(同上)
  • C -0-> E = ε-closure({q5}) = {q5}(q5-0?无边,move({q5},0) = ∅→ε-closure(∅) = ∅,但 ∅ 是死状态,记为Z)
  • C -1-> F = ε-closure({q5}) = {q5}(同上)
  • D -0-> G = ε-closure({q5}) = {q5}
  • D -1-> H = ε-closure({q5}) = {q5}

等等——这显然不对,C={q5}读0应无转移,但q5有ε->q4,q4有0->q5,所以move({q5},0)应为move(ε-closure({q5}),0)?不,子集构造中move(S,a)是S中所有状态经a直接到达的状态集合,不经过 ε。q5无0边,故move({q5},0)=∅。但q4有0边,而q4不在C中!C={q5},q4不在其中,所以move(C,0)=∅。正确。

继续:

  • B = {q1,q2,q4}读0→q4-0->q5→move = {q5}→ε-closure = {q5} = C
  • B读1→q4-1->q5→C
  • C = {q5}读0→∅→ 死状态Z
  • C读1→∅→Z
  • Z读0/1→Z

现在B需要连到101链:B中q2有ε->q4,但q4无ε->q3;q3由q5-ε->q4和q4无到q3的边,q3只能由q5的 ε-转移?不,q5有ε->q4,但q3是*子图的出口,由q5-ε->q3?我们构造时是q5-ε->q4(循环),q3由q2的 ε 边?不,q2-ε->q4是入口,出口是q5-ε->q3?标准 Thompson 对(0|1)*的构造中,q5应有ε->q3(退出边)。修正:在q5添加ε->q3。则ε-closure({q5}) = {q5, q3}(因q5-ε->q3),q3-ε->q6,所以move({q5},ε)包含q6,但move不走 ε。move({q5},a)仍为 ∅,但ε-closure({q5}) = {q5,q3},q3有ε->q6,所以ε-closure({q5})实际是{q5,q3,q6}?不,ε-closure只加 ε 可达,q3-ε->q6是 ε 边,所以ε-closure({q5}) = {q5} ∪ ε-closure({q3}) = {q5} ∪ {q3,q6}(若q3-ε->q6),再q6-1->q7,所以B读1到C={q5,q3,q6},C读1到move({q6},1)={q7},ε-closure({q7})={q7}。

这才是关键:*单元的出口q3必须有 ε 边到101入口q6,且q3的 ε-闭包必须包含q6,否则*结束后无法启动101匹配。我们之前漏了q3-ε->q6,导致C无法触发101链。

修正后,B = {q1,q2,q4},ε-closure不变;B读0/1到C = ε-closure({q5}) = {q5,q3,q6}(因q5-ε->q3,q3-ε->q6);C读1→q6-1->q7→move={q7}→ε-closure={q7};q7读0→q8;q8读1→q9;q9是终态,ε-closure({q9})={q9}。

此时 DFA 状态:A={q0},B={q1,q2,q4},C={q5,q3,q6},D={q7},E={q8},F={q9},G=Z(死状态),共 7 个。F是接受态。

3.3 生成 DFA 状态转移表:每个状态的语义解释

DFA 状态对应 NFA 状态集语义解释接受态?
A{q0}初始,未读任何字符否
B{q1,q2,q4}已读前置1,正处在 `(01)*的入口,等待0或1`
C{q5,q3,q6}`(01)*已结束(q3),101匹配已启动(q6),等待第一个1`
D{q7}101的第一个1已读,等待0否
E{q8}101的10已读,等待最后一个1否
F{q9}101完整读完,匹配成功是
Z∅死状态,任何输入均不可恢复否

注意:C状态同时包含q5(*循环中)、q3(*退出)、q6(101入口),意味着它既能继续*(读0/1回C),也能启动101(读1到D)。这正是*和后续字面量并存的语义体现。


4. DFA 最小化:为什么 7 个状态是理论下限?合并错误的 3 个致命信号

得到 7 状态 DFA 后,必须最小化。Hopcroft 算法或填表法均可,但手工操作易错。最小化的本质不是“减少状态数”,而是“合并行为等价的状态”——即对所有输入字符串,它们导向相同接受/拒绝结果。对1(0|1)*101,Z(死状态)必单独一类;F(终态)必单独一类;其余 5 个非终态需检验是否等价。

4.1 填表法实操:标记所有可区分的状态对

我们用填表法(Myhill-Nerode):

  • 初始划分:终态集{F},非终态集{A,B,C,D,E,Z};
  • 检查非终态集中每对(X,Y):是否存在输入a,使δ(X,a)和δ(Y,a)属于不同划分类。

DFA 转移(基于前述修正):

  • A -1-> B,A -0-> Z
  • B -1-> C,B -0-> C
  • C -1-> D,C -0-> Z
  • D -0-> E,D -1-> Z
  • E -1-> F,E -0-> Z
  • Z -0/1-> Z

第一步:Z与任何非Z状态可区分(因Z读0还是Z,而A读0是Z,但A读1是B,Z读1是Z,B非终态,Z非终态,暂不可分)。更可靠的是看终态:F是唯一终态,所以任何X若δ(X,a)=F,而Y无此转移,则X,Y可分。

  • E -1-> F,其他状态无a到F的转移(D-0->E,C-1->D,B-0/1->C,A-1->B),所以E与{A,B,C,D,Z}可分(因E能一步到终态)。
  • D -0-> E,E已分离,所以D与{A,B,C,Z}可分(因D能到E,而A读0到Z,Z与E不同类)。
  • C -1-> D,D已分离,所以C与{A,B,Z}可分。
  • B -0/1-> C,C已分离,A -1-> B,A -0-> Z,Z与C不同类,所以A与B可分(A-0->Z,B-0->C,Z和C不同类)。

最终划分:{A},{B},{C},{D},{E},{F},{Z}——7 个状态全部不可合并。这就是最小 DFA。

提示:常见错误是认为B和C可合并(都“在中间”),但B读0到C,C读0到Z,行为不同;或认为D和E可合并,但D读0到E,E读0到Z,且E读1到F,D读1到Z,完全不可分。

4.2 验证最小 DFA 的字符串覆盖能力

用 7 状态 DFA 测试边界字符串:

字符串预期路径是否接受
101是A-1→B-0→C-1→D-0→E-1→F✓
1101是A-1→B-1→C-1→D-0→E-1→F✓
1001否A-1→B-0→C-0→Z✓
1010否A-1→B-0→C-1→D-0→E-1→F,但输入还有0,F 读0→ ? 我们未定义F的转移!最小 DFA 中,F应有0/1到Z的边,因F是终态但非“吸收态”。所以1010: A-1→B-0→C-1→D-0→E-1→F-0→Z,拒绝。✓
1否A-1→B,结束,B 非终态✓
10否A-1→B-0→C,结束✓
1011是?101后1,但1011结尾是011,非101。路径:A-1→B-0→C-1→D-0→E-1→F-1→Z,拒绝。✓

全部符合。

4.3 避坑:DFA 构造中 4 个高频翻车点

现象:DFA 接受101但拒绝1101
原因:*单元的出口q3未连接到101入口q6,或q3-ε->q6缺失,导致*吃掉字符后无法启动101匹配。
解决:在 Thompson 构造(0|1)*时,确保退出状态q3有 ε 边到后续单元初态q6。

现象:DFA 有 8 个状态,化简后剩 7 个,但多出一个“悬空”状态
原因:子集构造中,ε-closure计算遗漏了q3的 ε 后继(如q3-ε->q6),导致生成了一个本应被ε-closure吸收的状态集。
解决:对每个move(S,a)结果,严格递归计算ε-closure,用栈或队列实现,勿凭记忆。

现象:最小化后状态数为 6,但1001被错误接受
原因:错误合并了C和Z(死状态),因C-0->Z,误以为C和Z行为相似;但C-1->D,Z-1->Z,D非终态,Z非终态,需进一步检查D和Z——D-0->E,E能到F,Z不能,故C和Z可分。
解决:填表法必须逐层推进,不能因一次转移相同就停止。

现象:DFA 在101后续字符上陷入死循环,不进入Z
原因:未为终态F定义0/1的转移边,或默认无定义即停机,但 DFA 要求全函数,必须补F-0->Z,F-1->Z。
解决:DFA 定义要求对每个状态和每个输入符号都有转移;未定义的边一律指向死状态Z。


5. 可视化与代码验证:用 Graphviz 画出最小 DFA,并用 Python 模拟器跑通全部测试用例

手工推演终需代码验证。我们用 Python 实现 DFA 模拟器,并用 Graphviz 生成状态图,确保所见即所得。

5.1 最小 DFA 的完整状态定义(7 状态)

状态集Q = {A, B, C, D, E, F, Z}
字母表Σ = {0, 1}
初态q0 = A
终态集F = {F}
转移函数δ:

当前状态输入0输入1
AZB
BCC
CZD
DEZ
EZF
FZZ
ZZZ

注意:B读0或1都到C,体现(0|1)*的任意性;C读1启动101,读0失败;D和E是101的计数器;F是唯一接受态,之后任何输入均坠入Z。

5.2 用 Graphviz 绘制 DFA:一眼看清状态语义

digraph DFA_101 { rankdir=LR; size="8,5" node [shape = circle]; A [label="A\nstart"]; B [label="B\nread first 1"]; C [label="C\n* ended, wait 1"]; D [label="D\nread 1 of 101"]; E [label="E\nread 10 of 101"]; F [label="F\naccept!\n101 matched"]; Z [label="Z\ndead state"]; A -> B [label="1"]; A -> Z [label="0"]; B -> C [label="0,1"]; C -> Z [label="0"]; C -> D [label="1"]; D -> E [label="0"]; D -> Z [label="1"]; E -> Z [label="0"]; E -> F [label="1"]; F -> Z [label="0,1"]; Z -> Z [label="0,1"]; node [shape = doublecircle]; F; }

渲染后,图清晰显示:A是入口,B是前置1的确认,C是决策点(*结束后选101或放弃),D/E/F是101的流水线,Z是黑洞。这种可视化比状态表更能暴露逻辑漏洞——例如,若C无0->Z边,则图中C会悬空,一眼可知错误。

5.3 Python DFA 模拟器:跑通 20 个测试用例

def dfa_simulate(input_str): # 状态映射 states = {'A':0, 'B':1, 'C':2, 'D':3, 'E':4, 'F':5, 'Z':6} current = 'A' trans = { 'A': {'0':'Z', '1':'B'}, 'B': {'0':'C', '1':'C'}, 'C': {'0':'Z', '1':'D'}, 'D': {'0':'E', '1':'Z'}, 'E': {'0':'Z', '1':'F'}, 'F': {'0':'Z', '1':'Z'}, 'Z': {'0':'Z', '1':'Z'} } accept_states = {'F'} for c in input_str: if c not in ['0','1']: return False if current not in trans or c not in trans[current]: return False current = trans[current][c] return current in accept_states # 测试用例 test_cases = [ ("101", True), ("1101", True), ("1001", False), ("1010", False), ("1", False), ("10", False), ("1011", False), ("0101", False <p> <a href="https://download.csdn.net/download/gaobbo86/509937" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:29:31

基于知识图谱的古诗词问答系统:Python+Neo4j完整实现

简介&#xff1a;基于知识图谱的古诗词问答系统完整源码包&#xff0c;面向计算机相关专业毕业生、课程设计与项目实战学习者&#xff0c;适合作为本科毕设、课程设计或期末大作业的完整参考。资源以Python后端、知识图谱数据与前端展示为核心&#xff0c;共521个文件&#xff…

作者头像 李华
网站建设 2026/10/13 1:29:28

语音短时时域分析:短时能量、过零率、自相关与AMDF详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:28:48

EMC设计前移:从原理图到量产的可量化风险评估方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:28:42

Python递归解迷宫:turtle可视化深度优先搜索与回溯

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:28:24

深度学习验证码识别:CNN多标签分类实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:28:00

OSG 3.6集成Qt 5.13:嵌入式三维渲染窗口的完整实现与踩坑记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华