1 COMS 3261 讲义 6B: NFA 和正则表达式练习题解答

📜 原文
📖 逐步解释
∑ 公式拆解
💡 数值示例
⚠️ 易错点
📝 总结
🎯 存在目的
🧠 直觉心智模型
💭 直观想象

11 COMS 3261 讲义 6B: NFA 和正则表达式练习题解答

📜 [原文1]

COMS 3261 讲义 6B: <br> NFA正则表达式练习题解答

Ziheng Huang 和 Kiru Arjunan<br>(致谢 Cindy Wang,2018 助教)

2024 秋季

📖 [逐步解释]

这份文档是哥伦比亚大学计算机科学理论课程(COMS W3261)的讲义解答,具体来说是关于非确定性有限自动机 (NFA)正则表达式 (Regular Expression) 的练习题解答。文档由 Ziheng Huang 和 Kiru Arjunan 编写,并感谢了 2018 年的助教 Cindy Wang 的贡献。这份材料适用于 2024 年秋季学期。它旨在帮助学生理解和掌握 NFA正则表达式的核心概念,以及它们之间的转换方法。

📝 [总结]

这是一个课程讲义的解答部分,专注于 NFA正则表达式的练习题。

🎯 [存在目的]

该文档的目的是为学生提供一个参考,使他们能够核对自己的作业答案,理解解题过程,并深化对 NFA正则表达式相关理论的认识。

🧠 [直觉心智模型]

您可以将这份文档看作是一本带有详细“官方题解”的练习册。当您完成 NFA正则表达式的练习后,可以翻阅这本题解,看看自己的答案是否正确,更重要的是,学习解决这类问题的标准思路和方法。

💭 [直观想象]

想象一下,您正在学习一门新的语言(计算理论的语言),这份文档就像是语言学习中的答案之书。每当您尝试翻译一个句子(解决一个问题)后,都可以打开这本书来检查自己的翻译是否地道、准确,并从中学习语法规则(理论知识)和表达技巧(解题方法)。

22 NFA

2.1 问题 1

📜 [原文2]

问题 1. 画出一个识别以下内容的 NFA

(a) 所有包含 101 的字符串。

📖 [逐步解释]

这个问题要求我们构造一个非确定性有限自动机 (NFA),该 NFA 能够识别所有包含子串 "101" 的二进制字符串。

NFA 的核心思想是“猜测和验证”。对于这个问题,NFA 需要“猜测”子串 "101" 何时开始,然后“验证”接下来的三个字符是否确实是 "101"。

  1. 起始状态 $q_0$:这是自动机的起点。在看到 "101" 子串之前,任何字符(0 或 1)的出现都只是“等待”阶段。因此,在 $q_0$ 状态,我们设置一个自循环,无论输入是 0 还是 1,状态都保持在 $q_0$。这表示 NFA 正在读取字符串的开头部分,但还没有找到 "101" 的迹象。
  2. 检测到 '1':当处于 $q_0$ 状态时,如果读入一个 '1',NFA 会进行“猜测”。它有两个选择:
    • 选择一(留在 $q_0$):它猜测这个 '1' 不是 "101" 子串的开始,于是继续停留在 $q_0$ 状态,等待下一个可能的 "101" 的开头。
    • 选择二(进入 $q_1$):它猜测这个 '1' 可能是 "101" 子串的开始。于是,它从 $q_0$ 转移到新状态 $q_1$。
  3. 检测到 '0':现在假设 NFA 处于 $q_1$ 状态(因为它刚刚看到了一个 '1')。如果下一个输入是 '0',这符合 "101" 模式的第二个字符。于是,NFA 从 $q_1$ 转移到状态 $q_2$。
  4. 检测到 '1' (完成):如果 NFA 处于 $q_2$ 状态(意味着它已经看到了 "10"),并且下一个输入是 '1',那么 "101" 子串就完整地出现了。此时,NFA 从 $q_2$ 转移到状态 $q_3$。
  5. 接受状态 $q_3$:状态 $q_3$ 是一个接受状态。一旦进入这个状态,就意味着字符串中已经成功找到了 "101" 这个子串。由于题目要求是“包含” "101",所以一旦找到,无论后面跟着什么字符,该字符串都应该被接受。因此,在 $q_3$ 状态,我们设置一个自循环,对于任何输入(0 或 1),状态都保持在 $q_3$。
  6. 失败路径:如果猜测失败了怎么办?例如,在 $q_1$ 状态(已看到 '1')时,如果下一个输入不是 '0' 而是 '1',那么 "10" 的模式就断了。在 NFA 中,如果没有为某个输入定义转移,那么这条计算路径就会“死亡”。这正是 NFA 的便利之处,我们只需要关心成功路径即可。

综上所述,构造出的 NFA 如下图所示:

  • $q_0$ 是起始状态
  • $q_0$ 上有一个指向自身的环,标签为 0, 1,表示在找到 "101" 的第一个 '1' 之前,可以消耗任意数量的 0 和 1。
  • 从 $q_0$ 到 $q_1$ 有一条标签为 1 的边,代表猜测 "101" 的开始。
  • 从 $q_1$ 到 $q_2$ 有一条标签为 0 的边。
  • 从 $q_2$ 到 $q_3$ 有一条标签为 1 的边。
  • $q_3$ 是接受状态
  • $q_3$ 上有一个指向自身的环,标签为 0, 1,表示一旦找到 "101",后面可以跟任意字符。
💡 [数值示例]
  • 示例 1:字符串 "01010"
  1. 开始于 $q_0$。
  2. 读入 '0':留在 $q_0$。
  3. 读入 '1':NFA 分裂成两条路径。一条留在 $q_0$,另一条转移到 $q_1$。
    • 路径 A: ...$q_0$
    • 路径 B: ...$q_1$
  4. 读入 '0':
    • 路径 A (在 $q_0$): 留在 $q_0$。
    • 路径 B (在 $q_1$): 转移到 $q_2$。
  5. 读入 '1':
    • 路径 A (在 $q_0$): 分裂,一条留在 $q_0$,一条到 $q_1$。
    • 路径 B (在 $q_2$): 转移到 $q_3$。
  6. 读入 '0':
    • 路径 B (在 $q_3$): 留在 $q_3$。其他路径我们不再关心。
  7. 字符串结束。由于至少有一条路径(路径 B)最终停在接受状态 $q_3$,所以字符串 "01010" 被接受。
  • 示例 2:字符串 "1100"
  1. 开始于 $q_0$。
  2. 读入 '1':分裂成两条路径。
    • 路径 A: $q_0$
    • 路径 B: $q_1$
  3. 读入 '1':
    • 路径 A (在 $q_0$): 分裂成 $q_0$ 和 $q_1$。
    • 路径 B (在 $q_1$): 路径死亡,因为没有从 $q_1$ 出发,输入为 '1' 的转移
  4. 读入 '0':
    • 路径 A' (来自 $q_0$): 留在 $q_0$。
    • 路径 A'' (来自 $q_1$): 转移到 $q_2$。
  5. 读入 '0':
    • 路径 A' (在 $q_0$): 留在 $q_0$。
    • 路径 A'' (在 $q_2$): 路径死亡。
  6. 字符串结束。所有路径都没有停在接受状态 $q_3$。因此字符串 "1100" 被拒绝。
⚠️ [易错点]
  1. 忘记起始状态的自循环:如果 $q_0$ 没有 (0,1) 的自循环,那么 NFA 将只能识别以 "101" 开头的字符串,而不能识别 "00101" 这样的字符串。
  2. 忘记接受状态的自循环:如果 $q_3$ 没有 (0,1) 的自循环,那么 NFA 将只能识别以 "101" 结尾的字符串,而不能识别 "10100" 这样的字符串。
  3. 将 NFA 当作 DFA 设计:如果试图为每个状态的所有可能输入都定义唯一的转移(例如,从 $q_1$ 收到 '1' 该去哪里),就会把问题复杂化。NFA 的优势在于可以忽略失败路径。
  4. 边界情况:字符串 "101" 本身应该被接受。空字符串 $\epsilon$ 不包含 "101",应该被拒绝。
📝 [总结]

我们设计了一个四状态的 NFA 来识别包含 "101" 的语言。该 NFA 利用非确定性来“猜测”子串的起始位置,并通过一条线性的状态路径来“验证”该子串。起始和结束状态的自循环确保了 "101" 可以出现在字符串的任何位置。

🎯 [存在目的]

这个问题的目的是检验对 NFA 基本构造的理解,特别是如何利用非确定性来简化对“包含某个子串”这类语言的识别。这与 DFA 的设计形成了鲜明对比,用 DFA 解决同样的问题会需要更多的状态来“记忆”已经匹配的部分。

🧠 [直觉心智模型]

把这个 NFA 想象成一个寻宝游戏。你有一张藏宝图,上面写着“找到连续的'1'、'0'、'1'三个标记”。

  1. $q_0$ 是你的大本营。你一直在周围闲逛($q_0$ 的自循环),寻找第一个标记 '1'。
  2. 每当你看到一个 '1',你都可以在原地继续闲逛,也可以选择相信“宝藏就在这里!”,然后迈出第一步,到达 $q_1$。
  3. 到了 $q_1$,你开始寻找第二个标记 '0'。如果找到了,就前进到 $q_2$。如果没找到(比如又看到了一个 '1'),那这条寻宝路线就失败了,你得从头再来。
  4. 到了 $q_2$,你寻找最后的标记 '1'。如果找到了,恭喜你,到达了藏宝地 $q_3$!
  5. 一旦到达 $q_3$,你就找到了宝藏,可以躺平了($q_3$ 的自循环),不管后面发生什么,你都已经成功了。
💭 [直观想象]

想象一条流水线,上面传送着一串由 0 和 1 组成的珠子。你有三个机器人($q_1, q_2, q_3$)排成一队。

  1. 你($q_0$)在流水线起点,看着珠子一个个过去。
  2. 当你看到一个 '1' 珠子时,你立刻复制一个自己,让复制体去启动第一个机器人 $q_1$。你自己则继续留在原地观望。
  3. 机器人 $q_1$ 启动后,只等待 '0' 珠子。如果等到了,它就启动机器人 $q_2$,然后自己下班。如果没等到,它就报废。
  4. 机器人 $q_2$ 启动后,只等待 '1' 珠子。如果等到了,它就启动机器人 $q_3$,然后自己下班。
  5. 机器人 $q_3$ 一旦被启动,就会亮起一盏绿灯,并且这盏灯永远不会熄灭。

只要最后流水线走完时,绿灯是亮的,就说明这串珠子是合格的。


📜 [原文3]

(b) $L=\left\{w \in\{0,1\}^{*} \mid w\right.$ 恰好有两个 0 或有偶数个 1$\}$

📖 [逐步解释]

这个问题要求我们构造一个 NFA,它接受的语言 $L$ 包含两类字符串:要么字符串中恰好有两个 0,要么字符串中有偶数个 1

这里的关键词是“或”,这强烈暗示了我们可以分别构造识别这两个子语言的自动机,然后用并集操作将它们组合起来。NFA 的一个巨大优势就是可以轻松地实现并集操作。

第一部分:构造识别“恰好有两个 0”的 NFA ($N_1$)

我们需要一个计数器来记录 0 的数量。

  1. 状态 $c_0$起始状态,表示到目前为止有 0 个 0。在这个状态下,可以读取任意数量的 1,状态不变。如果读到 0,就转移到下一个状态。
  2. 状态 $c_1$:表示到目前为止有 1 个 0。同样,可以读取任意数量的 1 而状态不变。如果读到第二个 0,就转移到下一个状态。
  3. 状态 $c_2$:表示到目前为止恰好有两个 0。这是一个接受状态。在此之后,不能再出现 0,否则 0 的数量就超过 2 了。所以,这个状态只允许读取 1,并且状态保持不变。

这个子自动机 ($c_0, c_1, c_2$) 是一个 DFA

第二部分:构造识别“偶数个 1”的 NFA ($N_2$)

我们需要一个计数器来记录 1 的奇偶性。

  1. 状态 $p_0$起始状态,表示到目前为止有偶数个 1(0 是偶数)。这是一个接受状态。如果读到 0,1 的奇偶性不变,所以状态不变。如果读到 1,1 的数量变为奇数,转移到下一个状态。
  2. 状态 $p_1$:表示到目前为止有奇数个 1。这不是一个接受状态。如果读到 0,状态不变。如果读到 1,1 的数量变为偶数,转移回 $p_0$。

这个子自动机 ($p_0, p_1$) 也是一个 DFA

第三部分:组合 $N_1$ 和 $N_2$ 得到最终的 NFA

并集的构造方法是:

  1. 创建一个新的起始状态 $q_0$。
  2. 从新的起始状态 $q_0$ 添加 $\epsilon$-转移(空转移)到原来两个自动机的起始状态,即 $c_0$ 和 $p_0$。

这样,当 NFA 开始处理一个字符串时,它会立即“猜测”这个字符串应该满足哪个条件:

  • 如果它猜测字符串有“恰好两个 0”,它就会沿着 $\epsilon$-转移进入 $c_0$ 开始计算。
  • 如果它猜测字符串有“偶数个 1”,它就会沿着另一条 $\epsilon$-转移进入 $p_0$ 开始计算。

只要最终至少有一条计算路径停在接受状态($c_2$ 或 $p_0$),整个字符串就被接受。

最终的 NFA 结构如下:

  • 新起始状态 $q_0$。
  • 从 $q_0$ 到 $c_0$ 有一条 $\epsilon$-转移
  • 从 $q_0$ 到 $p_0$ 有一条 $\epsilon$-转移
  • 上半部分(来自 $N_1$)
  • $c_0 \xrightarrow{1} c_0$, $c_0 \xrightarrow{0} c_1$
  • $c_1 \xrightarrow{1} c_1$, $c_1 \xrightarrow{0} c_2$
  • $c_2 \xrightarrow{1} c_2$
  • $c_2$ 是接受状态
  • 下半部分(来自 $N_2$)
  • $p_0 \xrightarrow{0} p_0$, $p_0 \xrightarrow{1} p_1$
  • $p_1 \xrightarrow{0} p_1$, $p_1 \xrightarrow{1} p_0$
  • $p_0$ 是接受状态
💡 [数值示例]
  • 示例 1:字符串 "010"
  1. 从 $q_0$ 开始,NFA 立即分裂到 $c_0$ 和 $p_0$。
  2. 路径 C (从 $c_0$ 开始):
    • 读 '0': $c_0 \rightarrow c_1$
    • 读 '1': $c_1 \rightarrow c_1$
    • 读 '0': $c_1 \rightarrow c_2$
    • 字符串结束,停在接受状态 $c_2$。
  3. 路径 P (从 $p_0$ 开始):
    • 读 '0': $p_0 \rightarrow p_0$
    • 读 '1': $p_0 \rightarrow p_1$
    • 读 '0': $p_1 \rightarrow p_1$
    • 字符串结束,停在非接受状态 $p_1$。
  4. 因为路径 C 成功了,所以字符串 "010" 被接受(因为它恰好有两个 0)。
  • 示例 2:字符串 "101"
  1. 从 $q_0$ 开始,分裂到 $c_0$ 和 $p_0$。
  2. 路径 C (从 $c_0$ 开始):
    • 读 '1': $c_0 \rightarrow c_0$
    • 读 '0': $c_0 \rightarrow c_1$
    • 读 '1': $c_1 \rightarrow c_1$
    • 字符串结束,停在非接受状态 $c_1$。
  3. 路径 P (从 $p_0$ 开始):
    • 读 '1': $p_0 \rightarrow p_1$
    • 读 '0': $p_1 \rightarrow p_1$
    • 读 '1': $p_1 \rightarrow p_0$
    • 字符串结束,停在接受状态 $p_0$。
  4. 因为路径 P 成功了,所以字符串 "101" 被接受(因为它有偶数个 1)。
  • 示例 3:字符串 "01010"
  1. 从 $q_0$ 开始,分裂到 $c_0$ 和 $p_0$。
  2. 路径 C (从 $c_0$ 开始):
    • 读 '0': $c_0 \rightarrow c_1$
    • 读 '1': $c_1 \rightarrow c_1$
    • 读 '0': $c_1 \rightarrow c_2$
    • 读 '1': $c_2 \rightarrow c_2$
    • 读 '0': $c_2$ 之后没有输入为 '0' 的转移,路径死亡。
  3. 路径 P (从 $p_0$ 开始):
    • 读 '0': $p_0 \rightarrow p_0$
    • 读 '1': $p_0 \rightarrow p_1$
    • 读 '0': $p_1 \rightarrow p_1$
    • 读 '1': $p_1 \rightarrow p_0$
    • 读 '0': $p_0 \rightarrow p_0$
    • 字符串结束,停在接受状态 $p_0$。
  4. 因为路径 P 成功了,所以字符串 "01010" 被接受(因为它有偶数个 1,尽管它有三个 0)。
⚠️ [易错点]
  1. 错误地合并状态:不能简单地认为两个子自动机的起始状态是同一个状态。使用新的起始状态和 $\epsilon$-转移是实现并集的标准和可靠方法。
  2. 对“恰好”理解错误:在构造 $N_1$ 时,一旦到达 $c_2$(两个 0),就不能再接受任何 0。这意味着从 $c_2$ 出发没有为输入 '0' 定义的转移
  3. 对“偶数”理解错误:0 是一个偶数。因此,对于 $N_2$,“偶数个 1”的起始状态 $p_0$(代表 0 个 1)本身就必须是接受状态。这意味着不包含 '1' 的字符串(如 "00")应该被 $N_2$ 部分接受。
  4. 边界情况:空字符串 $\epsilon$。
  5. 路径 C:$c_0$ 不是接受状态
  6. 路径 P:$p_0$ 是接受状态
  7. 所以 $\epsilon$ 被接受(因为它有 0 个 1,是偶数)。这是正确的。
📝 [总结]

这个问题展示了如何使用并集的构造方法来解决“或”类型的语言识别问题。我们首先为每个子条件(“恰好有两个 0”和“偶数个 1”)分别设计了独立的自动机,然后通过一个带 $\epsilon$-转移的新起始状态将它们组合成一个大的 NFA。这个 NFA 利用非确定性来同时探索两种可能性。

🎯 [存在目的]

这个问题的目的是考察学生对正规语言封闭性质(特别是并集)的理解,以及如何将这种理论上的构造方法应用到实际的 NFA 设计中。它强调了模块化设计的思想:将复杂问题分解为简单子问题,分别求解,然后组合。

🧠 [直觉心智模型]

想象一个公司招聘,有两个独立的部门在同时面试你。

  1. 部门 C(计数部):他们的要求是“你的简历里必须不多不少,正好提到过两次 '0' 项目”。面试官 $c_0$(0 次)、$c_1$(1 次)、$c_2$(2 次)会检查你的简历。只有面试官 $c_2$ 检查完后才会给你发 offer(接受状态)。
  2. 部门 P(奇偶校验部):他们的要求是“你的简历里提到 '1' 项目的次数必须是偶数”。面试官 $p_0$(偶数次)和 $p_1$(奇数次)会检查你的简历。只有面试官 $p_0$ 会给你发 offer。
  3. 你(字符串) 去公司前台 ($q_0$) 报道。前台小姐告诉你:“你可以选择去 C 部门面试,也可以选择去 P 部门面试,或者两边都去试试。”($\epsilon$-转移)。

只要最后你拿到了任何一个部门的 offer,你就被这家公司录用了(字符串被接受)。

💭 [直观想象]

想象一个分岔的管道系统。

  1. 水流(输入的字符串)从总入口 $q_0$ 进入。
  2. 入口处立刻有一个 T 型岔路口,水流被分成两股,分别流向管道系统 C 和管道系统 P。
  3. 管道 C 有三个阀门($c_0, c_1, c_2$)。水流中的 '0' 杂质会打开下一级阀门,'1' 杂质则让水流在当前阀门区域循环。如果 '0' 杂质超过两个,管道就会堵塞。只有水流最终从阀门 $c_2$ 流出,才算成功。
  4. 管道 P 有两个阀门 ($p_0, p_1$)。水流中的 '1' 杂质会让水流在两个阀门之间切换,'0' 杂质则不影响。只有当水流最终从阀门 $p_0$ 流出时,才算成功。
  5. 只要最后有任意一股水流从 C 或 P 的出口流出,就表示整个系统是通的(字符串被接受)。

2.2 问题 2

📜 [原文4]

(a) 该 NFA 识别的语言是什么?

状态 $q_{0}$ 不是接受状态。该 NFA 识别的语言是 $\emptyset$。

📖 [逐步解释]

这个问题展示了一个非常简单的 NFA,并询问它能识别哪种语言。

  1. 分析 NFA 结构:
    • NFA 只有一个状态,$q_0$。
    • $q_0$ 是起始状态(由指向它的箭头指示)。
    • $q_0$ 不是一个接受状态(它没有用双圈表示)。
    • $q_0$ 上有一个自循环,标签为 '0'。这意味着当 NFA 处于 $q_0$ 状态时,如果读入一个 '0',它会回到 $q_0$。
    • 没有为输入 '1' 定义任何转移
  2. 寻找接受路径:
    • 根据有限自动机的定义,一个字符串被接受,当且仅当在读完整个字符串后,自动机至少存在一条计算路径,使其最终停在一个接受状态上。
    • 在这个 NFA 中,唯一的状态 $q_0$ 不是接受状态。事实上,这个 NFA 根本没有接受状态
    • 因此,无论输入什么字符串(包括空字符串 $\epsilon$),计算路径的终点(必然是 $q_0$)永远不会是一个接受状态
    • 例如,对于字符串 "00":$q_0 \xrightarrow{0} q_0 \xrightarrow{0} q_0$。结束于 $q_0$,不是接受状态,拒绝。
    • 对于字符串 $\epsilon$:停在起始状态 $q_0$,不是接受状态,拒绝。
    • 对于字符串 "1":从 $q_0$ 开始,没有为 '1' 定义的转移,计算路径死亡,拒绝。
  3. 结论:
    • 由于没有任何字符串能够使这个 NFA 停在接受状态,所以它不接受任何字符串。
    • 不包含任何字符串的语言被称为空语言,用符号 $\emptyset$ 表示。
💡 [数值示例]
  • 示例 1:字符串 "0"

计算路径为 $q_0 \xrightarrow{0} q_0$。最终状态是 $q_0$,它不是接受状态。所以 "0" 被拒绝。

  • 示例 2:字符串 "" (空字符串 $\epsilon$)

计算从起始状态 $q_0$ 开始,不消耗任何输入。最终状态是 $q_0$,不是接受状态。所以 $\epsilon$ 被拒绝。

  • 示例 3:字符串 "010"

计算路径为 $q_0 \xrightarrow{0} q_0$。当读到 '1' 时,由于没有从 $q_0$ 出发,输入为 '1' 的转移,计算路径死亡。所以 "010" 被拒绝。

⚠️ [易错点]
  1. 混淆空语言 $\emptyset$ 和包含空字符串的语言 $\{\epsilon\}$
  2. 空语言 $\emptyset$ 是一个集合,里面什么元素都没有。这个 NFA 识别的就是空语言。
  3. 包含空字符串的语言 $\{\epsilon\}$ 是一个集合,里面有且只有一个元素,那就是空字符串 $\epsilon$。这两种语言是完全不同的。
  4. 认为有状态就一定能接受字符串:自动机可以存在,但其设计的结构可能导致它一个字符串也无法接受。关键在于是否有从起始状态接受状态的成功路径。
📝 [总结]

NFA 识别的语言是空语言 $\emptyset$,因为它没有任何接受状态。因此,任何计算路径都不可能在接受状态结束。

🎯 [存在目的]

这个问题的目的是检验对接受状态在语言识别中作用的基本理解。它通过一个极端例子强调了:没有接受状态的自动机,其识别的语言就是空语言

🧠 [直觉心智模型]

这就像一个只有一个房间的迷宫。你是从这个房间开始的(起始状态),但这个房间本身并不是出口(非接受状态)。更糟糕的是,这个迷宫根本就没有设置任何出口。所以无论你怎么走(处理输入字符串),你永远也走不出这个迷宫。因此,你永远无法“成功”完成任务。

💭 [直观想象]

想象一个自动售货机。你投币,按下按钮(输入字符串),但机器里没有任何商品(没有接受状态)。无论你做什么操作,都不可能掉出任何东西。这台机器的功能是“什么也不卖”,它能“卖出”的商品集合是空的。


📜 [原文5]

(b) 该 NFA 识别的语言是什么?

这里,$q_{0}$ 是一个接受状态,因此 $\epsilon$ 被接受。然而,由于没有从 $q_{0}$ 出发的转移,对于任何长度 $>0$ 的字符串都没有接受计算。该 NFA 识别的语言是 $\{\epsilon\}$。

注意:$\emptyset \neq\{\epsilon\}$

📖 [逐步解释]

这个问题展示了另一个简单的 NFA,并询问它识别的语言。

  1. 分析 NFA 结构:
    • NFA 只有一个状态,$q_0$。
    • $q_0$ 是起始状态(由指向它的箭头指示)。
    • $q_0$ 一个接受状态(由双圈表示)。
    • 这个 NFA 没有定义任何转移。从 $q_0$ 出发,无论输入是 '0' 还是 '1',都没有对应的转移箭头。
  2. 寻找接受路径:
    • 情况一:输入为空字符串 $\epsilon$
    • 根据定义,处理空字符串 $\epsilon$ 意味着自动机从起始状态开始,不消耗任何输入。
    • 因此,当输入为 $\epsilon$ 时,自动机停留在其起始状态 $q_0$。
    • 由于 $q_0$ 恰好是一个接受状态,所以空字符串 $\epsilon$ 被接受。
    • 情况二:输入为任何非空字符串 (长度 > 0)
    • 例如,考虑字符串 "0"。计算从起始状态 $q_0$ 开始。
    • 当自动机尝试读取第一个字符 '0' 时,它发现没有从 $q_0$ 出发,输入为 '0' 的转移
    • 此时,这条计算路径“死亡”。
    • 对于任何以 '0' 或 '1' 开头的字符串,其计算路径都会在第一步死亡。
    • 由于没有计算路径能够在读完非空字符串后停留在任何状态(更不用说接受状态了),所以所有长度大于 0 的字符串都被拒绝。
  3. 结论:
    • 这个 NFA 唯一能接受的字符串就是空字符串 $\epsilon$。
    • 因此,它识别的语言是只包含空字符串的语言,用集合表示为 $\{\epsilon\}$。
  4. 与前一题的对比:
    • 解答中特意强调 $\emptyset \neq \{\epsilon\}$。
    • 前一个 NFA (2a) 识别空语言 $\emptyset$,这是一个不包含任何元素的集合。
    • 当前的 NFA (2b) 识别语言 $\{\epsilon\}$,这是一个包含一个元素(即空字符串)的集合。
    • 这是计算理论中一个非常基础且重要的区别。
💡 [数值示例]
  • 示例 1:字符串 "" (空字符串 $\epsilon$)

计算从起始状态 $q_0$ 开始,不移动。最终状态是 $q_0$,它是一个接受状态。所以 $\epsilon$ 被接受。

  • 示例 2:字符串 "0"

计算从 $q_0$ 开始。当尝试读取 '0' 时,没有定义转移,路径死亡。所以 "0" 被拒绝。

  • 示例 3:字符串 "101"

计算从 $q_0$ 开始。当尝试读取 '1' 时,没有定义转移,路径死亡。所以 "101" 被拒绝。

⚠️ [易错点]
  1. 最大的易错点:混淆 $\emptyset$ 和 $\{\epsilon\}$。这在考试和理论讨论中是完全不同的两个概念。前者是空集,后者是包含空字符串这个特定元素的单元素集。
  2. 忽略起始状态就是接受状态:看到一个NFA,第一步就应该检查起始状态的性质。如果起始状态本身就是接受状态,那么语言中至少包含了 $\epsilon$。
  3. 认为没有转移就什么都不接受:虽然没有转移确实限制了非空字符串的接受,但这并不影响对 $\epsilon$ 的判断。
📝 [总结]

NFA 识别的语言是 $\{\epsilon\}$,即只包含空字符串的语言。这是因为它的起始状态本身就是一个接受状态,但没有任何转移来处理非空字符串。

🎯 [存在目的]

这个问题的目的是为了巩固对起始状态接受状态之间关系的理解,并清晰地区分空语言 $\emptyset$ 和包含空字符串的语言 $\{\epsilon\}$。它展示了一个只接受空字符串的最小自动机。

🧠 [直觉心智模型]

这个 NFA 就像一个“入门即巅峰”的测试。

  1. 测试的起点 ($q_0$) 本身就是终点线(接受状态)。
  2. 如果你选择“原地不动”(输入 $\epsilon$),你立刻就成功了。
  3. 但测试规则是,只要你敢迈出任何一步(输入任何非空字符),你脚下就会出现一个陷阱(没有转移),你直接就掉下去了(计算死亡)。
  4. 所以,唯一能通过这个测试的方法就是什么都不做。
💭 [直观想象]

想象一个非常奇怪的俱乐部。

  1. 俱乐部的大门 ($q_0$) 上挂着一个“欢迎光临”的牌子(接受状态)。
  2. 如果你站在门口,什么都不干(输入 $\epsilon$),你就已经被认为是“进入”了俱乐部。
  3. 但是,大门后面是一堵墙,没有任何通道可以让你走进俱乐部内部(没有转移)。只要你试图往里走一步(输入任何字符),你就会撞在墙上,被保安请出去(计算死亡)。
  4. 所以,这个俱乐部唯一能“接纳”的成员,就是那些“精神上”在门口站着的人。

2.3 问题 3

📜 [原文6]

问题 3. 使用子集构造法将此 NFA 转换为等价的 DFA

不要忘记标注接受状态

📖 [逐步解释]

这个问题要求我们将一个给定的 NFA 转换成一个等价的 DFA,方法是使用子集构造法

子集构造法的核心思想是:DFA 的每一个状态对应 NFA 的一个状态子集DFA 在某个状态下读入一个输入字符后,它会转移到一个新的状态,这个新状态对应的新子集,是原 NFA 从旧子集中的所有状态出发,经过该输入字符以及所有可能的 $\epsilon$-转移后,能够到达的所有状态的集合。

第一步:确定 NFA 的基本信息

  • 状态集 Q: $\{q_0, q_1, q_2\}$
  • 字母表 $\Sigma$: $\{a, b\}$
  • 起始状态: $q_0$
  • 接受状态 F: $\{q_2\}$
  • 转移函数 $\delta$:
  • $\delta(q_0, a) = \{q_1\}$
  • $\delta(q_0, b) = \{q_0\}$
  • $\delta(q_1, a) = \{q_2\}$
  • $\delta(q_1, b) = \{q_0, q_2\}$
  • $\delta(q_2, a) = \emptyset$
  • $\delta(q_2, b) = \{q_1\}$

第二步:开始构造 DFA

  1. DFA 的起始状态:
    • DFA 的起始状态是包含 NFA 起始状态的子集,并包括从它出发可以经过 $\epsilon$-转移到达的所有状态。
    • 在这个 NFA 中,没有 $\epsilon$-转移
    • 所以,DFA 的起始状态就是 $\{q_0\}$。我们将其标记为 DFA 的一个新状态,例如 S0
    • S0 = {q_0}
  2. 从 DFA 的起始状态 S0 出发进行转移:
    • 输入 'a':
    • S0 = {q_0} 中的每个状态出发,看在 NFA 中输入 'a' 能到达哪些状态。
    • 从 $q_0$ 输入 'a',到达 $\{q_1\}$。
    • 由于没有 $\epsilon$-转移,所以 DFA 从 S0 输入 'a' 后到达的新状态是 $\{q_1\}$。
    • 这是一个我们没见过的新子集,我们将其标记为 S1S1 = {q_1}
    • 所以,$\delta_{DFA}(S0, a) = S1$。
    • 输入 'b':
    • S0 = {q_0} 中的 $q_0$ 出发,输入 'b',到达 $\{q_0\}$。
    • 所以 DFA 从 S0 输入 'b' 后到达的新状态是 $\{q_0\}$,也就是 S0 本身。
    • 所以,$\delta_{DFA}(S0, b) = S0$。
  3. 处理新发现的状态 S1:
    • 我们现在有了一个新状态 S1 = {q_1},需要计算它的转移
    • 输入 'a':
    • 从 $q_1$ 输入 'a',到达 $\{q_2\}$。
    • 这是一个新子集,我们标记为 S2S2 = {q_2}
    • 所以,$\delta_{DFA}(S1, a) = S2$。
    • 输入 'b':
    • 从 $q_1$ 输入 'b',到达 $\{q_0, q_2\}$。
    • 这是一个新子集,我们标记为 S3S3 = {q_0, q_2}
    • 所以,$\delta_{DFA}(S1, b) = S3$。
  4. 处理新发现的状态 S2:
    • 我们现在有了新状态 S2 = {q_2}
    • 输入 'a':
    • 从 $q_2$ 输入 'a',到达 $\emptyset$(空集)。
    • 空集是一个新子集,我们标记为 S_dead (死亡状态)。S_dead = {}
    • 所以,$\delta_{DFA}(S2, a) = S_{dead}$。
    • 输入 'b':
    • 从 $q_2$ 输入 'b',到达 $\{q_1\}$。
    • 子集 $\{q_1\}$ 是我们已经见过的 S1
    • 所以,$\delta_{DFA}(S2, b) = S1$。
  5. 处理新发现的状态 S3:
    • 我们现在有了新状态 S3 = {q_0, q_2}
    • 输入 'a':
    • 从 $q_0$ 输入 'a' 到达 $\{q_1\}$。
    • 从 $q_2$ 输入 'a' 到达 $\emptyset$。
    • 将结果并集起来:$\{q_1\} \cup \emptyset = \{q_1\}$。
    • 子集 $\{q_1\}$ 是我们已经见过的 S1
    • 所以,$\delta_{DFA}(S3, a) = S1$。
    • 输入 'b':
    • 从 $q_0$ 输入 'b' 到达 $\{q_0\}$。
    • 从 $q_2$ 输入 'b' 到达 $\{q_1\}$。
    • 将结果并集起来:$\{q_0\} \cup \{q_1\} = \{q_0, q_1\}$。
    • 这是一个新子集,我们标记为 S4S4 = {q_0, q_1}
    • 所以,$\delta_{DFA}(S3, b) = S4$。
  6. 处理新发现的死亡状态 S_dead:
    • S_dead = {}
    • 输入 'a': 从空集中的状态出发,什么也到不了,结果还是空集 $\emptyset$。所以 $\delta_{DFA}(S_{dead}, a) = S_{dead}$。
    • 输入 'b': 同理,$\delta_{DFA}(S_{dead}, b) = S_{dead}$。
    • 这是一个陷阱状态,一旦进入就无法离开。
  7. 处理新发现的状态 S4:
    • S4 = {q_0, q_1}
    • 输入 'a':
    • 从 $q_0$ 输入 'a' 到达 $\{q_1\}$。
    • 从 $q_1$ 输入 'a' 到达 $\{q_2\}$。
    • 并集: $\{q_1\} \cup \{q_2\} = \{q_1, q_2\}$。
    • 这是一个新子集,我们标记为 S5S5 = {q_1, q_2}
    • 所以,$\delta_{DFA}(S4, a) = S5$。
    • 输入 'b':
    • 从 $q_0$ 输入 'b' 到达 $\{q_0\}$。
    • 从 $q_1$ 输入 'b' 到达 $\{q_0, q_2\}$。
    • 并集: $\{q_0\} \cup \{q_0, q_2\} = \{q_0, q_2\}$。
    • 子集 $\{q_0, q_2\}$ 是我们已经见过的 S3
    • 所以,$\delta_{DFA}(S4, b) = S3$。
  8. 处理新发现的状态 S5:
    • S5 = {q_1, q_2}
    • 输入 'a':
    • 从 $q_1$ 输入 'a' 到达 $\{q_2\}$。
    • 从 $q_2$ 输入 'a' 到达 $\emptyset$。
    • 并集: $\{q_2\} \cup \emptyset = \{q_2\}$。
    • 子集 $\{q_2\}$ 是我们已经见过的 S2
    • 所以,$\delta_{DFA}(S5, a) = S2$。
    • 输入 'b':
    • 从 $q_1$ 输入 'b' 到达 $\{q_0, q_2\}$。
    • 从 $q_2$ 输入 'b' 到达 $\{q_1\}$。
    • 并集: $\{q_0, q_2\} \cup \{q_1\} = \{q_0, q_1, q_2\}$。
    • 这是一个新子集,我们标记为 S6S6 = {q_0, q_1, q_2}
    • 所以,$\delta_{DFA}(S5, b) = S6$。
  9. 处理新发现的状态 S6:
    • S6 = {q_0, q_1, q_2}
    • 输入 'a':
    • 从 $q_0$ 到 $\{q_1\}$。从 $q_1$ 到 $\{q_2\}$。从 $q_2$ 到 $\emptyset$。
    • 并集: $\{q_1, q_2\}$,即 S5
    • 所以,$\delta_{DFA}(S6, a) = S5$。
    • 输入 'b':
    • 从 $q_0$ 到 $\{q_0\}$。从 $q_1$ 到 $\{q_0, q_2\}$。从 $q_2$ 到 $\{q_1\}$。
    • 并集: $\{q_0, q_1, q_2\}$,即 S6 本身。
    • 所以,$\delta_{DFA}(S6, b) = S6$。
  10. 确定 DFA 的接受状态:
    • DFA 的一个状态是接受状态,当且仅当它对应的 NFA 状态子集包含至少一个 NFA 的接受状态
    • NFA 的接受状态是 $\{q_2\}$。
    • 检查我们所有的 DFA 状态:
    • S0 = {q_0}: 不含 $q_2$,非接受。
    • S1 = {q_1}: 不含 $q_2$,非接受。
    • S2 = {q_2}: 包含 $q_2$,是接受状态
    • S3 = {q_0, q_2}: 包含 $q_2$,是接受状态
    • S_dead = {}: 不含 $q_2$,非接受。
    • S4 = {q_0, q_1}: 不含 $q_2$,非接受。
    • S5 = {q_1, q_2}: 包含 $q_2$,是接受状态
    • S6 = {q_0, q_1, q_2}: 包含 $q_2$,是接受状态

第三步:绘制最终的 DFA

将上述所有转移关系和接受状态画成一个状态图,就得到了最终的 DFA

💡 [数值示例]
  • 示例 1:字符串 "ab"
  • NFA 路径: $q_0 \xrightarrow{a} q_1 \xrightarrow{b} \{q_0, q_2\}$。因为最终状态集 $\{q_0, q_2\}$ 中包含接受状态 $q_2$,所以 NFA 接受 "ab"。
  • DFA 路径:
  1. 起始状态 {q_0} 开始。
  2. 读入 'a',转移{q_1}
  3. 读入 'b',转移{q_0, q_2}
  4. 字符串结束,停在 {q_0, q_2}。因为这个状态是接受状态,所以 DFA 接受 "ab"。
    • 结果一致。
  • 示例 2:字符串 "aba"
  • NFA 路径: $q_0 \xrightarrow{a} q_1 \xrightarrow{b} q_2 \xrightarrow{a} \text{死亡}$。另一条路径 $q_0 \xrightarrow{a} q_1 \xrightarrow{b} q_0 \xrightarrow{a} q_1$。最终停在非接受状态,NFA 拒绝 "aba"。
  • DFA 路径:
  1. {q_0} 开始。
  2. 读入 'a',到 {q_1}
  3. 读入 'b',到 {q_0, q_2}
  4. 读入 'a',到 {q_1}
  5. 字符串结束,停在 {q_1}。因为这个状态是非接受状态,所以 DFA 拒绝 "aba"。
    • 结果一致。
⚠️ [易错点]
  1. 忘记处理 $\epsilon$-闭包:在这个特定的 NFA 中没有 $\epsilon$-转移,但如果有,必须在每次计算转移后都计算新子集的 $\epsilon$-闭包(即从新子集中的状态出发,只通过 $\epsilon$-转移能到达的所有状态的集合)。
  2. DFA 接受状态判断错误:只要 NFA 状态子集中有一个是原 NFA 的接受状态,这个子集对应的 DFA 状态就是接受状态。不是要求全部。
  3. 遗漏状态:必须系统地处理所有新生成的子集状态,直到没有新的子集出现为止。使用表格或队列来跟踪未处理的子集是个好方法。
  4. 空集/死亡状态:从某个子集出发,对于某个输入,可能无路可走。这会转移到空集状态 $\emptyset$。这个空集状态是 DFA 的一个合法的(非接受)状态,通常是一个陷阱状态。
📝 [总结]

子集构造法是一个将任意 NFA(或带 $\epsilon$-转移NFA)机械化地转换为等价 DFA 的算法。其核心在于,DFA 的每个状态模拟了 NFA 在读入相同输入后可能处于的所有状态的集合。通过系统地探索这些状态子集及其转移,我们可以构建出完整的 DFA

🎯 [存在目的]

这个问题的目的是检验学生是否掌握了子集构造法这一核心算法。该算法不仅在理论上证明了 NFADFA 的等价性(即它们都能且只能识别正规语言),在实践中也是正则表达式引擎等工具实现的基础。

🧠 [直觉心智模型]

想象一下,NFA 是一个有多重人格的特工。在执行任务时(读取字符串),他可以同时“分身”到多个地点(处于多个状态)。

DFA 是一个观察这个特工的指挥官。指挥官无法同时跟踪每一个“分身”,但他有一个全景地图。

  1. DFA 的每个状态,就是指挥官地图上的一个“包围圈”,这个圈圈住了特工所有可能的分身位置(NFA 的状态子集)。
  2. 当新的情报(输入字符)传来,指挥官不需要关心每个分身具体怎么跑的,他只需要根据规则,直接计算出下一个“包- 围圈”应该画在哪里。
  3. 只要在任务结束时,指挥官的“包围圈”里包含了任何一个“成功地点”(NFA 的接受状态),指挥官就判定整个任务成功。
💭 [直观想象]

把 NFA 想象成一个在多维宇宙中穿梭的旅行者。每一步,他都可能分裂成多个自己,走向不同的宇宙(状态)。

子集构造法 就是上帝视角。你不是去跟踪每一个分裂的旅行者,而是把所有旅行者在某个时刻所处的宇宙打包成一个“元宇宙”(DFA 的状态)。

  1. DFA 的一次转移,就是从一个“元宇宙”整体演化到下一个“元- 宇宙”。
  2. 如果某个“元宇宙”里包含了“天堂”这个宇宙(NFA 的接受状态),那么这个“元宇宙”就被标记为“好的元宇宙”(DFA 的接受状态)。

最终,你看的是整个“元宇宙”的演化路径,而不是单个旅行者的轨迹。

33 正规语言的封闭性质

3.1 a) 并集下的封闭性

3.1.1 问题 4

📜 [原文7]

$L_{1}=\left\{w \in\{0,1\}^{*} \mid w\right.$ 有偶数个 0$\}$

$L_{2}=\left\{w \in\{0,1\}^{*} \mid w\right.$ 不包含两个连续的 1$\}$

$L_{1}$ 的 DFA

$L_{2}$ 的 DFA

$L_{1} \cup L_{2}$ 的 NFA

📖 [逐步解释]

这个问题展示了如何利用正规语言并集运算下的封闭性来构造一个新的自动机。封闭性意味着,如果 $L_1$ 和 $L_2$ 都是正规语言,那么它们的并集 $L_1 \cup L_2$ 也必然是正规语言。我们可以通过构造一个识别 $L_1 \cup L_2$ 的 NFA 来证明这一点。

第一步:分析两个语言及其 DFA

  • 语言 $L_1$: 包含偶数个 0 的所有二进制字符串。
  • DFA for $L_1$:
  • $q_0$: 起始状态,表示已看到偶数个 0 (包括 0 个)。这是接受状态
  • $q_1$: 表示已看到奇数个 0。这不是接受状态
  • 转移: 读到 '1' 不改变 0 的奇偶性,状态不变。读到 '0' 改变 0 的奇偶性,状态在 $q_0$ 和 $q_1$ 之间切换。
  • 语言 $L_2$: 不包含连续两个 '1' ("11") 的所有二进制字符串。
  • DFA for $L_2$:
  • $p_0$: 起始状态,最近没有看到 '1',或者刚看到 '0'。这是接受状态
  • $p_1$: 刚刚看到了一个 '1'。这也是接受状态
  • $p_2$: 已经看到了连续的 "11"。这是一个“陷阱”状态或“死亡”状态,非接受,并且任何输入都会留在这里。
  • 转移:
  • 在 $p_0$,读 '0' 留在 $p_0$,读 '1' 到 $p_1$。
  • 在 $p_1$,读 '0' 回到 $p_0$,读 '1' 则触发了 "11",进入陷阱状态 $p_2$。
  • 在 $p_2$,任何输入都留在 $p_2$。

第二步:构造识别 $L_1 \cup L_2$ 的 NFA

构造并集NFA 的标准方法非常直观:

  1. 创建一个全新的起始状态,我们称之为 $s$。
  2. 从新起始状态 $s$ 出发,画两条 $\epsilon$-转移(空转移)。一条指向 $L_1$ 的 DFA 的起始状态 $q_0$,另一条指向 $L_2$ 的 DFA 的起始状态 $p_0$。
  3. 保留原来两个 DFA 的所有状态和转移
  4. 保留原来两个 DFA 的所有接受状态。在这个例子中,$q_0, p_0, p_1$ 都是最终 NFA 的接受状态

工作原理:

当这个新的 NFA 开始处理一个输入字符串时,它会从状态 $s$ 开始。由于有两条 $\epsilon$-转移,它会立刻、不消耗任何输入地“分裂”成两条并行的计算路径:

  • 路径 1: 进入 $q_0$,然后完全按照 $L_1$ 的 DFA 规则进行计算。
  • 路径 2: 进入 $p_0$,然后完全按照 $L_2$ 的 DFA 规则进行计算。

在字符串处理完毕后,只要这两条路径中至少有一条停在了接受状态,整个字符串就被这个新的 NFA 接受。这完美地对应了并集的定义:一个字符串属于 $L_1 \cup L_2$,当且仅当它属于 $L_1$ 属于 $L_2$。

💡 [数值示例]
  • 示例 1:字符串 "001"
  • 在 $L_1$ 的 DFA 中: $q_0 \xrightarrow{0} q_1 \xrightarrow{0} q_0 \xrightarrow{1} q_0$。停在接受状态 $q_0$。所以 "001" $\in L_1$。
  • 在 $L_2$ 的 DFA 中: $p_0 \xrightarrow{0} p_0 \xrightarrow{0} p_0 \xrightarrow{1} p_1$。停在接受状态 $p_1$。所以 "001" $\in L_2$。
  • 在并集 NFA 中:
  1. 从 $s$ 开始,分裂到 $q_0$ 和 $p_0$。
  2. 路径1 (模拟 $L_1$): ... 最终停在 $q_0$ (接受)。
  3. 路径2 (模拟 $L_2$): ... 最终停在 $p_1$ (接受)。
  4. 因为至少有一条路径成功,所以 "001" 被并集 NFA 接受。
  • 示例 2:字符串 "11"
  • 在 $L_1$ 的 DFA 中: $q_0 \xrightarrow{1} q_0 \xrightarrow{1} q_0$。停在接受状态 $q_0$ (因为有 0 个 0,是偶数)。所以 "11" $\in L_1$。
  • 在 $L_2$ 的 DFA 中: $p_0 \xrightarrow{1} p_1 \xrightarrow{1} p_2$。停在非接受状态 $p_2$。所以 "11" $\notin L_2$。
  • 在并集 NFA 中:
  1. 从 $s$ 开始,分裂。
  2. 路径1 (模拟 $L_1$): 最终停在 $q_0$ (接受)。
  3. 路径2 (模拟 $L_2$): 最终停在 $p_2$ (拒绝)。
  4. 因为路径 1 成功了,所以 "11" 被并集 NFA 接受。
  • 示例 3:字符串 "011"
  • 在 $L_1$ 的 DFA 中: $q_0 \xrightarrow{0} q_1 \xrightarrow{1} q_1 \xrightarrow{1} q_1$。停在非接受状态 $q_1$。所以 "011" $\notin L_1$。
  • 在 $L_2$ 的 DFA 中: $p_0 \xrightarrow{0} p_0 \xrightarrow{1} p_1 \xrightarrow{1} p_2$。停在非接受状态 $p_2$。所以 "011" $\notin L_2$。
  • 在并集 NFA 中: 两条路径都失败了。所以 "011" 被并集 NFA 拒绝。
⚠️ [易错点]
  1. 试图合并原始起始状态:一个常见的错误是直接将 $q_0$ 和 $p_0$ 当作一个状态,或者让它们之间有某种转移。正确的构造方法是引入一个全新的、中立的起始状态
  2. 忘记 $\epsilon$-转移:$\epsilon$-转移是这个构造的核心,它实现了非确定性的“选择”。
  3. 处理接受状态出错:并集 NFA 的接受状态就是原来所有 DFA 的接受状态的集合。不要遗漏,也不要添加额外的。
📝 [总结]

该示例清晰地展示了正规语言关于并集封闭性的构造性证明。通过引入一个带 $\epsilon$-转移的新起始状态,我们可以将两个独立的自动机“粘合”在一起,创建一个新的 NFA,它能识别原来两个语言的并集

🎯 [存在目的]

这个问题的目的是让学生亲手实践封闭性的构造。理论上知道“正规语言并集封闭”是一回事,能够根据这个理论画出具体的自动机是另一回事。这个练习巩固了理论与实践的联系。

🧠 [直觉心智模型]

这就像你有两张不同俱乐部的会员卡 ($L_1$ 和 $L_2$)。现在你想办一张“联合会员卡”($L_1 \cup L_2$),这张卡在两个俱乐部都能使用。

  1. 这张联合会员卡本身 ($s$) 只是一个凭证。
  2. 当你到达一个场所时,你需要出示这张卡。保安会给你两个选择($\epsilon$-转移):“你可以选择激活你的 $L_1$ 会员身份进入,或者激活你的 $L_2$ 会员身份进入。”
  3. 只要你满足其中任何一个俱乐部的入场要求,你就可以进入。
💭 [直观想象]

想象有两套不同的安检系统,分别用于检查“0 病毒”偶数和“11 病毒”不存在。

  1. 旅客(字符串)来到一个分流口 ($s$)。
  2. 系统会立刻复制这个旅客,一个送去安检系统1(检查 $L_1$),另一个送去安检系统2(检查 $L_2$)。
  3. 只要旅客能通过其中任意一个安检系统,他就可以登机(被接受)。

3.2 b) 连接下的封闭性

3.2.1 问题 5

📜 [原文8]

$L_{1}=\left\{w \in\{0,1\}^{*} \mid w\right.$ 有奇数个 1 或恰好有两个 0$\}$。$L_{2}=\left\{w \in\{0,1\}^{*} \mid w\right.$ 以 101 结尾$\}$。

📖 [逐步解释]

这个问题展示了如何利用正规语言连接(concatenation)运算下的封闭性来构造一个新的自动机。如果 $L_1$ 和 $L_2$ 是正规语言,那么它们的连接 $L_1L_2 = \{xy \mid x \in L_1, y \in L_2\}$ 也是正规语言

第一步:分析两个语言及其 NFA

  • 语言 $L_1$: 包含奇数个 1 恰好有两个 0 的所有二进制字符串。
  • 这是一个“或”类型的语言,所以它的 NFA (我们称之为 $N_1$) 本身就是通过并集构造得到的(类似于问题 4b)。
  • 从图中可以看出,$N_1$ 有一个起始状态 $s_0$,它通过 $\epsilon$-转移连接到两个子模块:
  • 上半部分:识别“奇数个 1”。有两个状态,一个代表偶数个 1(非接受),一个代表奇数个 1(接受)。
  • 下半部分:识别“恰好有两个 0”。有三个状态,分别代表 0 个、1 个、2 个 0。最后一个状态是接受状态
  • $N_1$ 的接受状态是 $q_1$ 和 $c_2$。
  • 语言 $L_2$: 以 "101" 结尾的所有二进制字符串。
  • 它的 NFA (我们称之为 $N_2$) 的构造类似于问题 1a,但是是识别“以...结尾”,所以接受状态后面没有自循环。
  • $p_0 \xrightarrow{0,1} p_0$:等待 "101" 的开始。
  • $p_0 \xrightarrow{1} p_1$:匹配到 '1'。
  • $p_1 \xrightarrow{0} p_2$:匹配到 '10'。
  • $p_2 \xrightarrow{1} p_3$:匹配到 '101'。$p_3$ 是接受状态

第二步:构造识别 $L_1L_2$ 的 NFA

构造连接 $N_1N_2$ 的 NFA 的标准方法是:

  1. 将 $N_1$ 的所有接受状态连接到 $N_2$ 的起始状态。这个连接通过 $\epsilon$-转移实现。
  2. $N_1$ 的起始状态 成为新 NFA起始状态
  3. $N_2$ 的接受状态 成为新 NFA接受状态
  4. $N_1$ 原来的接受状态不再是接受状态(因为它们现在是中间站,而不是终点)。

工作原理:

这个构造模拟了语言连接的定义 $xy$。

  • 首先,自动机从 $N_1$ 的起始状态 $s_0$ 开始,处理输入字符串的前半部分。
  • 如果这部分字符串能在 $N_1$ 中走到一个接受状态(比如 $q_1$ 或 $c_2$),这就意味着前半部分字符串 $x$ 属于 $L_1$。
  • 此时,自动机不停止,而是通过一条 $\epsilon$-转移,“跳”到 $N_2$ 的起始状态 $p_0$。这个跳转不消耗任何输入字符。
  • 然后,自动机从 $p_0$ 开始,继续处理输入字符串的后半部分。
  • 如果后半部分字符串 $y$ 能让 $N_2$ 走到它的接受状态 $p_3$,那么就意味着 $y \in L_2$。
  • 整个过程成功完成,字符串 $xy$ 被接受。

NFA 的非确定性在这里至关重要,因为它需要“猜测”在哪里分割字符串,即在哪里从 $N_1$ 跳转到 $N_2$。

💡 [数值示例]
  • 示例 1:字符串 "1101"
  • 我们期望它被接受,因为可以分割成 $x="1" \in L_1$ (奇数个 1) 和 $y="101" \in L_2$ (以 101 结尾)。
  • 在连接 NFA 中的路径:
  1. 从 $s_0$ 开始,通过 $\epsilon$-转移到 $q_0$ (偶数个1) 和 $c_0$ (0个0)。
  2. 我们关注能成功的路径:走 $q_0$ 这条线。
  3. 读入 '1':$q_0 \xrightarrow{1} q_1$。此时 $q_1$ 是 $N_1$ 的一个接受状态
  4. NFA 做出猜测:在这里分割字符串!通过 $\epsilon$-转移从 $q_1$ 跳转到 $N_2$ 的起始状态 $p_0$。
  5. 现在自动机在 $p_0$ 状态,字符串剩下 "101"。
  6. 读入 '1':$p_0 \xrightarrow{1} p_1$。
  7. 读入 '0':$p_1 \xrightarrow{0} p_2$。
  8. 读入 '1':$p_2 \xrightarrow{1} p_3$。
  9. 字符串结束,停在 $p_3$,这是整个连接 NFA 的接受状态。所以字符串 "1101" 被接受。
  • 示例 2:字符串 "010"
  • 它不应该被接受,因为无法分割成 $x \in L_1$ 和 $y \in L_2$。
  • 例如,如果 $x=\epsilon$,$\epsilon \in L_1$ (0个1是偶数),但 $y="010" \notin L_2$。
  • 如果 $x="0" \notin L_1$。
  • 如果 $x="01" \in L_1$ (奇数个1),但 $y="0" \notin L_2$。
  • ... 无论如何分割,都无法满足条件。在 NFA 中,这意味着所有可能的计算路径最终都会失败。
⚠️ [易错点]
  1. 忘记取消 $N_1$ 的接受状态:在连接构造中, $N_1$ 的接受状态变成了“中转站”,它们本身不再是最终的接受状态。如果忘记取消,会导致接受不应接受的字符串(例如,只属于 $L_1$ 但后面没有跟 $L_2$ 字符串的情况)。
  2. 连接点错误:$\epsilon$-转移必须从 $N_1$ 的所有接受状态出发,连接到 $N_2$ 的起始状态
  3. 起始/接受状态指定错误:新 NFA 的起始状态就是 $N_1$ 的起始状态接受状态就是 $N_2$ 的接受状态
📝 [总结]

该示例展示了连接运算的封闭性构造。通过添加 $\epsilon$-转移,将第一个自动机 ($N_1$) 的终点连接到第二个自动机 ($N_2$) 的起点,我们构建了一个能够识别两个语言连接NFA。这个过程就像是顺序执行两个任务,完成了第一个才能开始第二个。

🎯 [存在目的]

此问题旨在让学生掌握连接运算的封闭性构造方法。这是继并集星号运算之后,将简单正规语言组合成更复杂正规语言的又一基本工具。

🧠 [直觉心智模型]

这就像一个两段式的接力赛。

  1. 第一棒选手(自动机 $N_1$)从起点($N_1$ 起始状态)出发。
  2. 他必须跑到指定的交接区($N_1$ 的接受状态)才能交接。
  3. 在交接区,他把接力棒(控制权)通过一次“瞬移”($\epsilon$-转移)交给第二棒选手。
  4. 第二棒选手(自动机 $N_2$)从他自己的起跑线($N_2$ 起始状态)出发,跑完剩下的路程。
  5. 只有当第二棒选手冲过终点线($N_2$ 接受状态),整个团队才算获胜。
💭 [直观想象]

想象你在玩一个闯关游戏。游戏有两关。

  1. 你从第一关的入口($N_1$ 起始状态)开始玩。
  2. 你必须成功打通第一关,到达第一关的出口($N_1$ 接受状态)。
  3. 在出口处,有一个传送门($\epsilon$-转移),它会立即把你传送到第二关的入口($N_2$ 起始状态)。
  4. 然后你开始玩第二关。
  5. 只有当你成功打通第二关,到达最终的宝藏室($N_2$ 接受状态),你才算赢得了整个游戏。

3.3 c) 克莱尼星号下的封闭性

📜 [原文9]

问题 6. 我们提供了尝试失败的例子——当然,还有许多其他成功的例子。

  1. 令 $L=\{10\}$。则 $L^{*}=\left\{(10)^{n} \mid n \geq 0\right\}$。虽然 $\epsilon \notin L$,但根据克莱尼星号运算的定义,$\epsilon \in L^{*}$。在这里,我们看到尝试 1 不接受空串

$L$ 的 DFA

$L^{*}$ 的错误尝试 1

📖 [逐步解释]

这个问题通过一系列失败的尝试,来引导我们理解构造克莱尼星号 (Kleene Star) 的正确方法。克莱尼星号 $L^*$ 的定义是:由 $L$ 中的字符串任意连接 0 次或多次形成的所有字符串的集合。即 $L^* = L^0 \cup L^1 \cup L^2 \cup \dots$,其中 $L^0 = \{\epsilon\}$。

失败尝试 1:简单地将接受状态变起始状态,或加回指的边

这个尝试的核心错误在于没有正确处理空字符串 $\epsilon$。

  • 语言 L: $\{10\}$。它的 DFA 很简单:$q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$ (接受)。
  • 语言 $L^*$: $\{\epsilon, 10, 1010, 101010, \dots\}$。
  • 错误构造 1: 将原 DFA 的接受状态 $q_2$ 直接用一条边指回起始状态 $q_0$。
  • 分析错误:
  1. 空字符串 $\epsilon$: 语言 $L^*$ 必须包含 $\epsilon$。在这个错误的 NFA 中,起始状态 $q_0$ 不是接受状态。要接受 $\epsilon$,自动机必须在不消耗任何输入的情况下停在接受状态。显然,这个构造做不到。因此,这个构造至少漏掉了 $\epsilon$。
  2. 接受 "10": $q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$。$q_2$ 是接受状态,可以接受。
  3. 接受 "1010": $q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2 \xrightarrow{\text{错误构造的边?}} q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$。这里假设从 $q_2$ 有一条 $\epsilon$ 边回到 $q_0$。这样可以接受 "1010"。
    • 核心缺陷: 最根本的问题是它不接受 $\epsilon$。任何对 $L$ 的星号运算 $L^*$,只要 $L$ 本身不是只包含 $\epsilon$ 的语言或者空语言,那么 $L^*$ 都会包含 $\epsilon$。而原语言 $L=\{10\}$ 不包含 $\epsilon$,所以它的 DFA 的起始状态不是接受状态。简单地加一条返回的边无法改变起始状态的性质。

正确的构造方法应该是什么样?

  1. 处理 $\epsilon$: 为了接受 $\epsilon$,最直接的方法是让起始状态本身就是接受状态
  2. 处理重复: 为了接受 $L$ 中字符串的多次重复,需要从原自动机的接受状态返回到起始状态

正确的构造(稍后会看到)是:

  1. 创建一个新的起始状态 $s_{new}$,并让它成为接受状态(这就解决了 $\epsilon$ 的问题)。
  2. 从 $s_{new}$ 画一条 $\epsilon$-转移到旧的起始状态 $q_0$。
  3. 从旧的所有接受状态(这里是 $q_2$)画一条 $\epsilon$-转移回到旧的起始状态 $q_0$(实现循环)。
💡 [数值示例]
  • 示例:字符串 $\epsilon$
  • 期望: 属于 $L^*$,应被接受。
  • 错误构造 1: 停在 $q_0$,非接受。失败
⚠️ [易错点]
  1. 首要易错点:忘记 $\epsilon$。这是星号运算最容易被忽略但又最基本的组成部分。一个语言 $L$ 的星号闭包 $L^*$ 总是包含空字符串 $\epsilon$。
  2. 构造过于简单:直接在原 DFA 上“小修小补”通常是行不通的,因为 DFA 的结构很僵化。引入新的状态和 $\epsilon$-转移来构造 NFA 是更通用和可靠的方法。
📝 [总结]

失败尝试 1 的主要教训是:构造 $L^*$ 时,必须确保空字符串 $\epsilon$ 被接受。一个简单的方法是让起始状态成为接受状态,但这种直接修改在原图上可能不可行。

🎯 [存在目的]

通过展示一个看似合理但实际错误的构造,这个问题强迫我们思考克莱尼星号的完整定义,特别是 $L^0 = \{\epsilon\}$ 这一部分。它为引出更复杂的失败案例和最终的正确构造方法做了铺垫。

🧠 [直觉心智模型]

想象一个任务是“重复吃掉一个‘10’套餐”。

  1. $L$ 的 DFA 是“吃一个‘10’套餐”的流程。
  2. $L^*$ 是“可以不吃,也可以吃任意多份‘10’套餐”。
  3. 错误构造 1 相当于说:“你吃完一份后,可以回到起点再吃一份”。但它忘了规定“你也可以选择一份都不吃”。这个规则(不吃也算完成任务)是至关重要的。
💭 [直观想象]

想象一个旋转门。

  1. $L$ 的 DFA 是走过一条从 A 到 B 的普通走廊。
  2. $L^*$ 是一个可以让你在 A 和 B 之间无限次来回的系统,并且你甚至可以站在系统外,一次都不走,也算“参与”了。
  3. 错误构造 1 相当于在 B 点装了一个传送门,可以把你送回 A 点。但它没有在 A 点本身设置一个“成功”的标记。你必须至少走完一次全程才能到达 B 点。这不符合“可以一次都不走”的要求。

📜 [原文10]

  1. 令 $L=\left\{w \in\{0,1\}^{*} \mid \mathrm{w}\right.$ 以 01 结尾$\}$。则 $L^{*}=\{\epsilon\} \cup\left\{w \in\{0,1\}^{*} \mid \mathrm{w}\right.$ 以 01 结尾$\}$。让我们画出 $L$ 的 NFA 和 $L^{*}$ 的错误 NFA

$L^{*}$ 的错误尝试 2

右侧的 NFA 接受 $\epsilon$ 以及来自 $\left\{w \in\{0,1\}^{*} \mid \mathrm{w}\right.$ 以 01 结尾$\}$ 的字符串。然而,它还接受了额外的错误字符串,例如不在 $L^{*}$ 中的 000。事实上,由于 $q_{0}$ 对 0 和 1 的自转移,它识别的是 $\Sigma^{*}$。因此右侧的 NFA 并不识别 $L^{*}$。

📖 [逐步解释]

失败尝试 2:将起始状态直接设为接受状态

这个尝试吸取了上一个失败的教训,试图解决 $\epsilon$ 的问题。

  • 语言 L: 所有以 "01" 结尾的字符串。
  • $L$ 的 NFA: $q_0$ 有 (0,1) 自循环,然后 $q_0 \xrightarrow{0} q_1 \xrightarrow{1} q_2$ (接受)。
  • 语言 $L^*$: $\{\epsilon\} \cup L$。这里有一个有趣的性质:由于 $L$ 是“以...结尾”的语言,任何两个 $L$ 中字符串的连接,结果仍然是以 "01" 结尾,所以 $L \cdot L \subseteq L$。因此,$L^*$ 就简化为了 $\{\epsilon\} \cup L$。
  • 错误构造 2: 直接将原 NFA 的起始状态 $q_0$ 变成接受状态
  • 分析错误:
  1. 它能接受正确的字符串吗?
    • 接受 $\epsilon$: 能。因为起始状态 $q_0$ 现在是接受状态
    • 接受 $L$ 中的字符串 (如 "101"): 能。$q_0 \xrightarrow{1} q_0 \xrightarrow{0} q_1 \xrightarrow{1} q_2$。停在 $q_2$,是接受状态
  2. 它接受了不该接受的字符串吗?
    • 是的,这是问题的关键。
    • 考虑字符串 "0"。计算路径是 $q_0 \xrightarrow{0} q_0$。字符串结束,停在 $q_0$。由于这个构造把 $q_0$ 变成了接受状态,所以 "0" 被错误地接受了。
    • 但 "0" 并不在 $L^* = \{\epsilon\} \cup L$ 中,因为它既不是 $\epsilon$,也不以 "01" 结尾。
    • 同理,任何不以 "01" 结尾的非空字符串,比如 "11", "000", "1010",都会停在 $q_0$ 或 $q_1$,但因为 $q_0$ 是接受的,所以只要能回到 $q_0$ 的前缀都会被接受。实际上,由于 $q_0$ 的自循环,任何字符串都能在 $q_0$ 结束一个计算分支,所以这个错误的 NFA 接受了所有字符串 $\Sigma^*$。
  • 核心缺陷: 污染了起始状态。原 NFA 的起始状态 $q_0$ 具有“正在读取字符串,但尚未完成匹配”的语义。将其强行变为接受状态,就破坏了这种语义,导致所有尚未完成匹配的前缀都被错误地接受了。
💡 [数值示例]
  • 示例:字符串 "00"
  • 期望: 不属于 $L^*$,应被拒绝。
  • 错误构造 2: $q_0 \xrightarrow{0} q_0 \xrightarrow{0} q_0$。停在 $q_0$,而 $q_0$ 被设为接受状态错误地接受了
⚠️ [易错点]
  1. 治标不治本:为了解决一个问题(接受 $\epsilon$),引入了一个更严重的问题(接受了所有不该接受的字符串)。这在算法设计中是常见的陷阱。
  2. 忽略状态的语义:自动机中的每个状态都有其特定的含义(例如“已匹配...”、“等待...”、“失败”等)。修改一个状态的性质(比如是否接受)会根本性地改变其语义,必须谨慎。
📝 [总结]

失败尝试 2 的教训是:不能通过简单地改变原自动机状态的性质来构造 $L^*$。特别是,将具有“进行中”语义的起始状态变成接受状态,会引入大量非预期的接受情况。正确的做法是物理地增加一个全新的状态来专门处理 $\epsilon$ 的接受。

🎯 [存在目的]

这个问题揭示了星号运算构造的另一个难点:如何优雅地加入 $\epsilon$ 而不干扰原有的识别逻辑。它强调了“不要在原地修改,而要增加新结构”的设计原则。

🧠 [直觉心智模型]

这好比你想给一个正在进行的游戏增加一个“直接胜利”的选项。

  1. 原游戏($L$ 的 NFA)是你必须按部就班打怪升级,最后才能赢。
  2. 错误构造 2 相当于你在游戏开始的第一个房间(起始状态 $q_0$)门口直接挂了一个“你赢了”的牌子。
  3. 结果是,玩家一进游戏就赢了 ($\epsilon$ 被接受)。但问题是,玩家在第一个房间里随便逛几圈再出来(处理了 "00" 等字符串后回到 $q_0$),也看到了“你赢了”的牌子,于是他也以为自己赢了,但这不符合规则。
💭 [直观想象]

想象你在烤一个蛋糕。

  1. 原流程($L$ 的 NFA)是:准备材料 ($q_0$) -> 搅拌 ($q_1$) -> 烘烤 ($q_2$) -> 蛋糕出炉 (接受)。
  2. 你想增加一个选项,就是“可以不烤蛋糕也算完成任务”。
  3. 错误构造 2 的做法是,你在“准备材料”这个步骤的台子上放了一个牌子:“任务完成!”。
  4. 结果是,你啥也不干,站在台子前,就算完成任务 (接受 $\epsilon$)。但问题是,你拿了个鸡蛋又放回去,又拿了点面粉又放回去(在 $q_0$ 状态自循环),一抬头,看到“任务完成!”的牌子,你也以为自己完成任务了。但你手上根本没有蛋糕,这显然是错误的。

📜 [原文11]

  1. 此构造实际上与尝试 2 中的构造相同。对于相同的 $L=\{w \in \{0,1\}^{*} \mid$ w 以 01 结尾$\}$,让我们画出 $L$ 的 NFA 和 $L^{*}$ 的错误 NFA

同样,虽然这个错误构造的 NFA 接受 $\epsilon$ 和来自 $\left\{w \in\{0,1\}^{*} \mid \mathrm{w}\right.$ 以 01 结尾$\}$ 的字符串,它也接受 $\Sigma^{*}$。这是因为由于到新接受状态的 $\epsilon$-转移,任何到达 $q_{0}$ 的计算都会被自动接受。

📖 [逐步解释]

失败尝试 3:增加一个新接受状态,并从起始状态 $\epsilon$-转移过去

这个尝试是上一个的变体,它试图通过增加一个全新的状态来避免“污染”起始状态

  • 语言 L 和 $L^*$: 同上。
  • 错误构造 3:
  1. 保留原 NFA 不变。
  2. 创建一个新的状态 $q_{new}$,并将其设为接受状态
  3. 从原 NFA 的起始状态 $q_0$ 到这个新接受状态 $q_{new}$ 画一条 $\epsilon$-转移
  4. 让 $q_0$ 仍然是起始状态
  • 分析错误:
  • 这个构造在逻辑上和失败尝试 2 是等价的。为什么呢?
  • 在 NFA 中,如果一个状态 $A$ 可以通过 $\epsilon$-转移到达一个接受状态 $B$,那么状态 $A$ 本身在功能上也等同于一个接受状态
  • 因为任何计算路径只要到达了 $A$,它就可以不消耗任何输入,立刻“免费”跳转到 $B$,从而使得整个路径成为一条接受路径。
  • 在这个构造中,$q_0$ 可以通过 $\epsilon$-转移到达新的接受状态 $q_{new}$。因此,任何停在 $q_0$ 的计算路径实际上都是成功的。
  • 这和直接把 $q_0$ 设为接受状态(失败尝试 2)造成了完全相同的后果:所有能回到 $q_0$ 的前缀都被错误地接受了,导致整个 NFA 接受了所有字符串 $\Sigma^*$。
  • 核心缺陷: 仍然是起始状态被“污染”了,只不过这次是间接污染。通过 $\epsilon$-转移接受的性质从 $q_{new}$ “传递”给了 $q_0$。
💡 [数值示例]
  • 示例:字符串 "00"
  • 期望: 拒绝。
  • 错误构造 3:
  1. 起始状态 $q_0$ 开始。
  2. 读入第一个 '0',停在 $q_0$。
  3. 读入第二个 '0',停在 $q_0$。
  4. 字符串结束,路径停在 $q_0$。
  5. 从 $q_0$ 可以通过 $\epsilon$-转移到达接受状态 $q_{new}$。
  6. 因此,存在一条接受路径 $q_0 \xrightarrow{0} q_0 \xrightarrow{0} q_0 \xrightarrow{\epsilon} q_{new}$。
  7. 字符串 "00" 被错误地接受了
⚠️ [易错点]
  1. 低估 $\epsilon$-转移的威力:$\epsilon$-转移不仅是连接工具,它还会传递“可接受性”。在分析 NFA 时,一个状态的 $\epsilon$-闭包(它能通过任意多步 $\epsilon$-转移到达的所有状态的集合)非常重要。如果一个状态的 $\epsilon$-闭包里包含接受状态,那这个状态本身就起到了接受状态的作用。
  2. 新状态的引入位置错误:引入新状态是正确的方向,但必须放在正确的位置。新起始状态应该是整个结构的“最高层”,而不是从旧的起始状态引出一个分支。
📝 [总结]

失败尝试 3 的教训是,通过 $\epsilon$-转移起始状态连接到一个新的接受状态,等同于将起始状态本身变成接受状态,这同样会引入错误。正确的构造需要一个全新的、独立的起始状态

🎯 [存在目的]

这个问题进一步深化了对 NFA 行为,特别是 $\epsilon$-转移如何影响“接受”判定的理解。它排除了又一个看似可行但错误的构造方案,为最终的正确方案清除了障碍。

🧠 [直觉心智模型]

回到“直接胜利”的游戏。

  1. 错误构造 3 相当于,游戏开始的第一个房间 ($q_0$) 本身不是出口,但房间里有一个“免费传送门”($\epsilon$-转移),可以直接把你送到终点 ($q_{new}$)。
  2. 这和在房间门口直接挂“你赢了”的牌子(失败尝试 2)有什么区别?没有区别。玩家只要能回到第一个房间,就能通过传送门直接胜利。
💭 [直观想象]

回到烤蛋糕的想象。

  1. 错误构造 3 是,在“准备材料”的台子 ($q_0$) 旁边,你放了一个按钮,上面写着“一键完成”($\epsilon$-转移到 $q_{new}$)。
  2. 结果还是和之前一样。你一进厨房就可以按按钮,直接“完成”任务。你准备了一下材料又放回去,还是可以按按钮“完成”任务。这仍然是错误的。

📜 [原文12]

  1. 让我们看看 $L=\{10\}$。则 $L^{*}=\left\{(10)^{n} \mid n \geq 0\right\}$

新的 NFA 接受字符串 $\{\epsilon, 10\}$。然而,它不接受 $L^{*}$ 中的任何其他字符串,例如 1010。

📖 [逐步解释]

失败尝试 4:从接受状态 $\epsilon$-转移到新接受状态,并让新状态成为循环点

这个尝试是之前所有失败尝试的某种混合,想法更加复杂,但也同样存在缺陷。

  • 语言 L: $\{10\}$。
  • 语言 $L^*$: $\{\epsilon, 10, 1010, \dots\}$。
  • 错误构造 4:
  1. 保留原 NFA ($q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$)。
  2. 创建一个新的状态 $q_{new}$,并设为接受状态
  3. 从原 NFA 的接受状态 $q_2$ 到 $q_{new}$ 画一条 $\epsilon$-转移
  4. 让 $q_{new}$ 成为新的起始状态
  5. 从 $q_{new}$ 到原 NFA 的起始状态 $q_0$ 画一条 $\epsilon$-转移
  • 分析错误:
  1. 它能接受 $\epsilon$ 吗?
    • 能。因为新的起始状态 $q_{new}$ 本身就是接受状态
  2. 它能接受 "10" 吗?
    • 能。路径是 $q_{new} \xrightarrow{\epsilon} q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$。停在 $q_2$ (非接受),但可以继续 $\epsilon$-转移到 $q_{new}$ (接受)。所以接受。
  3. 它能接受 "1010" 吗?
    • 不能。这是问题的关键。
    • 让我们追踪路径:$q_{new} \xrightarrow{\epsilon} q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2 \xrightarrow{\epsilon} q_{new}$。到这里,字符串 "10" 被消耗完毕,自动机回到了起始状态 $q_{new}$。
    • 现在要处理后半部分的 "10"。自动机会再次沿着路径 $q_{new} \xrightarrow{\epsilon} q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$。
    • 但是,当它从 $q_2$ 再次尝试进行 $\epsilon$-转移时,它只能到达 $q_{new}$。它无法回到 $q_0$ 去开始下一次循环。
    • 这个构造中,循环的路径是 $q_0 \to q_1 \to q_2 \to q_{new} \to q_0$。但是,字符串的消耗只发生在 $q_0 \to q_1 \to q_2$ 这一段。一旦到了 $q_{new}$,再想开始新一轮的 "10" 匹配,就必须再次回到 $q_0$。而这个构造中,从 $q_2$ 无法直接回到 $q_0$。
  • 核心缺陷: 循环路径设置错误。正确的循环应该是完成一次匹配后,能回到开始下一次匹配的地方。这个构造让它回到了一个“终点兼起点”($q_{new}$),但从这个点出发,虽然能到达 $q_0$,但完成匹配后($q_2$)却无法再次回到 $q_0$。它把“完成一次”和“准备下一次”的路径搞混了。
💡 [数值示例]
  • 示例:字符串 "1010"
  • 期望: 接受。
  • 错误构造 4:
  1. $q_{new} \xrightarrow{\epsilon} q_0$
  2. 消耗 "10": $q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$
  3. $q_2 \xrightarrow{\epsilon} q_{new}$
  4. 此时 "10" 消耗完毕,状态回到 $q_{new}$。
  5. 现在要消耗第二个 "10"。
  6. $q_{new} \xrightarrow{\epsilon} q_0$
  7. 消耗 "10": $q_0 \xrightarrow{1} q_1 \xrightarrow{0} q_2$
  8. 字符串结束,停在 $q_2$。$q_2$ 不是接受状态
  9. 可以继续 $\epsilon$-转移到 $q_{new}$,这是一个接受状态。所以 "1010" 似乎可以被接受。
    • 等等,原文的分析说不接受 "1010"。让我们重新仔细审视图和原文的逻辑。
    • 原文的图示可能与我的描述略有不同。让我们严格按照图示来分析。
    • 图示中,新的起始状态是 $q_{start}$,新的接受状态是 $q_{accept}$。
    • $q_{start} \xrightarrow{\epsilon} q_0$
    • $q_2 \xrightarrow{\epsilon} q_{accept}$
    • $q_{start}$ 也是 $q_{accept}$。
    • $q_{accept} \xrightarrow{\epsilon} q_0$。
    • 这个构造确实很混乱。让我们遵循原文的结论:“它不接受 $L^*$ 中的任何其他字符串,例如 1010”。这暗示了循环机制是有问题的。
    • 最清晰的解释是:当一次 "10" 匹配完成到达 $q_2$ 后,它可以通过 $\epsilon$ 到达接受状态。但如果要开始下一次匹配,必须回到 $q_0$。这个构造中,从接受状态返回 $q_0$ 的路径可能没有被正确设置,或者说,这个跳转逻辑使得连续匹配无法实现。
    • 让我们重新审视标准构造:(1) 新建一个起始/接受状态 $S_{new}$。(2) $S_{new} \xrightarrow{\epsilon} q_0$。(3) 从原接受状态 $q_2 \xrightarrow{\epsilon} S_{new}$。这个构造是可以正确工作的!$q_0 \to q_1 \to q_2 \to S_{new} \to q_0 \dots$ 这样可以循环。
    • 那么为什么原文中的这个构造失败了?原文的图示可能是想表达一个特定的错误思想。该图中,新的接受状态没有画出到 $q_0$ 的 $\epsilon$ 边,它只接受了一次转移。如果从 $q_2$ 到达新的接受状态后,就没有出路了,那自然无法循环。原文的图示确实没有从那个新的双圈状态画出任何边。那么它的结论就是对的:这个 NFA 只能走 $q_{start} \to q_0 \to q_1 \to q_2 \to q_{accept}$ 一次。所以只能接受 $\epsilon$ (直接停在 $q_{start}$) 和 "10"。
📝 [总结]

失败尝试 4 的教训是,循环结构必须被精确地设计。必须确保完成一次匹配后,能够无缝地回到准备开始下一次匹配的状态。错误的连接会导致循环中断,从而无法识别重复多次的字符串。

最终正确的克莱尼星号构造方法

经过以上所有失败的探索,正确的构造方法浮出水面:

对于一个识别语言 $L$ 的自动机 $N_L$(起始状态 $q_{start,L}$,接受状态集 $F_L$):

  1. 创建一个全新的状态 $q_{new}$
  2. 将 $q_{new}$ 同时设为新 NFA 的起始状态接受状态。这保证了 $\epsilon$ 被接受。
  3. 从 $q_{new}$ 到 $N_L$ 的原起始状态 $q_{start,L}$ 添加一条 $\epsilon$-转移。这允许从头开始一次匹配。
  4. 从 $N_L$ 的每一个接受状态 $q \in F_L$ 到这个新的状态 $q_{new}$ 添加一条 $\epsilon$-转移。这表示一次成功的匹配已完成。

这个构造结合了 $q_{new} \to q_{start,L}$ 和 $q \to q_{new}$,形成了一个大循环 $q_{new} \to q_{start,L} \to \dots \to q \to q_{new}$,从而完美地实现了任意多次重复,且简洁地处理了 $\epsilon$ 的情况。

🎯 [存在目的]

这个系列失败案例的目的是通过排除法,让学生深刻理解正确构造方法的每一个组成部分都是不可或缺的,并理解它们各自的作用:新起始/接受状态用于处理 $\epsilon$ 和提供循环的“枢纽”,而 $\epsilon$ 边则负责“进入循环”和“完成一次循环并返回枢纽”。

3.4 d) 更多运算

3.4.1 问题 7a (逆转)

📜 [原文13]

对于 7a,我将给出一个非常正式的证明,以便你了解如何详细地完成这些证明。

问题 7a (逆转)。证明思路:为了构造一个识别 $L^{R}$ 的 DFA $B$,我们复制识别 $L$ 的 DFA $A$,反转所有箭头,将 $B$ 的起始状态定义为 $A$ 的接受状态,并将 $B$ 的接受状态定义为 $A$ 的起始状态。由于 $A$ 中可能有多个接受状态(它们现在是 $B$ 中的“起始状态”,而起始状态只能有一个),我们创建一个新的起始状态,通过 $\epsilon$ 转移到所有这些状态,从而得到一个识别 $L^{R}$ 的 NFA

存在一个 DFA $A=\left(Q_{A}, \Sigma, \delta_{A}, q_{0}, F_{A}\right)$ 使得 $L(A)=L$。构造如下 NFA $B=\left(Q_{B}, \Sigma, \delta_{B}, q_{0}^{\prime}, F_{B}\right)$,其中:

  • $Q_{B}=Q_{A} \cup\left\{q_{0}^{\prime}\right\}$。我们沿用来自 $A$ 的旧状态,但添加一个新起始状态 $q_{0}^{\prime}$。
  • $\delta_{B}\left(q_{0}^{\prime}, \epsilon\right)=F_{A}$。新起始状态对 $A$ 中所有以前的接受状态都有 epsilon 转移
  • 对于所有 $r \in Q_{B}$ 和 $a \in \Sigma, \delta_{B}(r, a)=\left\{q \in Q_{A} \mid \delta_{A}(q, a)=r\right\}$。我们将来自每个状态 $r \in Q_{B}$ 的转移定义为结束于 $A$ 中所有转移到相同 $r$ 的状态。换句话说,我们正在反转箭头。
  • $F_{B}=\left\{q_{0}\right\}$。$B$ 中的接受状态是 $A$ 的起始状态

$L$ 的 DFA

$L^{R}$ 的 NFA

📖 [逐步解释]

这个问题要求证明正规语言对于逆转 (reversal) 运算是封闭的。一个语言 $L$ 的逆转 $L^R$ 是将 $L$ 中每个字符串都反写过来得到的语言。例如,如果 $L = \{ab, cde\}$,那么 $L^R = \{ba, edc\}$。

证明思路是构造性的:如果我们有一个识别 $L$ 的自动机 $A$,我们能否通过改造它来得到一个识别 $L^R$ 的自动机 $B$?

直观想法: 如果字符串 $w$ 能让自动机 $A$ 从起始状态走到接受状态,那么它的逆转字符串 $w^R$ 应该能让新自动机 $B$ 从原来的接受状态“倒着走”回到原来的起始状态

这启发我们进行以下改造:

  1. 反转所有转移箭头: 如果在 $A$ 中有 $q_i \xrightarrow{a} q_j$,那么在 $B$ 中就应该有 $q_j \xrightarrow{a} q_i$。
  2. 交换起始和接受状态:
    • $A$ 的起始状态应该成为 $B$ 的接受状态
    • $A$ 的接受状态应该成为 $B$ 的起始状态

出现的问题:

  • 一个 DFA (或 NFA) 只能有一个起始状态,但它可能有很多个接受状态。当我们把 $A$ 的所有接受状态都变成 $B$ 的“起始状态”时,就违反了这个规则。
  • 一个状态对于一个输入,在 DFA 中只有一个出边。反转后,一个状态对于一个输入,可能有很多个出边(因为在原 DFA 中,可能有很多状态通过同一个字符转移到这个状态)。因此,反转后的自动机通常是一个 NFA,而不是 DFA

标准构造方法 (生成 NFA):

为了解决上述问题,我们构造一个 NFA $B$ 来识别 $L^R$,步骤如下:

  1. 状态: 保留 $A$ 的所有状态,并增加一个全新的起始状态 $q_0'$。
  2. 起始状态: 新的 NFA $B$ 的起始状态就是这个 $q_0'$。
  3. 转移:
    • 反转箭头: 对于 $A$ 中的每一个转移 $\delta_A(q, a) = r$,我们在 $B$ 中添加一个转移 $\delta_B(r, a)$ 包含 $q$。
    • 处理多“起始”点: 从新的起始状态 $q_0'$ 画 $\epsilon$-转移到 $A$ 中所有接受状态 $F_A$。这相当于说:“你可以从任何一个原来的终点开始你的‘逆向旅程’。”
  4. 接受状态: 将 $A$ 的原起始状态 $q_0$ 设为 $B$ 的唯一接受状态

图示例子:

  • $L$ 的 DFA: 识别以 'b' 结尾的语言 $L = (a \cup b)^*b$。
  • $q_0$ (起始), $q_1$ (接受)。
  • $q_0 \xrightarrow{a} q_0$, $q_0 \xrightarrow{b} q_1$。
  • $q_1 \xrightarrow{a} q_0$, $q_1 \xrightarrow{b} q_1$。
  • 构造 $L^R$ 的 NFA: $L^R$ 是以 'b' 开头的语言。
  1. 新起始状态: $q_0'$。
  2. 新接受状态: $\{q_0\}$。
  3. 反转箭头:
    • $q_0 \xleftarrow{a} q_0$, $q_1 \xleftarrow{b} q_0$
    • $q_0 \xleftarrow{a} q_1$, $q_1 \xleftarrow{b} q_1$
  4. 添加 $\epsilon$-转移: 从 $q_0'$ 到原接受状态 $q_1$ 添加 $\epsilon$-转移
    • 这样得到的 NFA (图示右侧),它的起始状态是 $q_0'$,接受状态是 $q_0$。让我们看看它是否识别以 'b' 开头的语言。
    • 对于 "ba":$q_0' \xrightarrow{\epsilon} q_1 \xrightarrow{b} q_1 \xrightarrow{a} q_0$。停在接受状态 $q_0$,接受。正确。
    • 对于 "b":$q_0' \xrightarrow{\epsilon} q_1 \xrightarrow{b} q_1$。停在非接受状态 $q_1$,拒绝。等等,"b" 应该被接受!
    • 啊哈,图示中有一个微妙之处:仔细看右图,从 $q_0'$ 出发有两条 $\epsilon$-转移,一条到 $q_0$,一条到 $q_1$。这是因为原图的 $q_0$ 状态没有被明确标为非接受,如果 $L$ 包含 $\epsilon$,那么 $q_0$ 也是接受状态。但在这个例子里,$L$ 不含 $\epsilon$。我们假设只有 $q_1$ 是接受。那么,从 $q_0'$ 只需 $\epsilon$-转移到 $q_1$。
    • 再次检查 "b": $q_0' \xrightarrow{\epsilon} q_1 \xrightarrow{b} q_1$。停在 $q_1$,非接受。为什么?因为原语言是 (a|b)b,而逆转 b(a|b) 包含 "b"。
    • 让我们重新审视原图:原图 $L$ 的 DFA 描述的是“以b结尾”。$q_0$ 读a留在 $q_0$,读b到 $q_1$。$q_1$ 读a到 $q_0$,读b留在 $q_1$。$q_0$ 是起始, $q_1$ 是接受。这是正确的。
    • 那么 $L^R$ 的 NFA 为什么拒绝了 "b"?让我们重新走一遍构造。
    • $A$ 的接受状态是 $F_A = \{q_1\}$。
    • $B$ 的起始状态 $q_0'$,接受状态 $F_B = \{q_0\}$。
    • $\delta_B(q_0', \epsilon) = \{q_1\}$。
    • $\delta_A(q_0, a)=q_0 \implies q_0 \in \delta_B(q_0, a)$。
    • $\delta_A(q_1, a)=q_0 \implies q_1 \in \delta_B(q_0, a)$。所以 $\delta_B(q_0, a) = \{q_0, q_1\}$。(图中画的箭头是从 $q_0$ 到 $q_1$)
    • $\delta_A(q_0, b)=q_1 \implies q_0 \in \delta_B(q_1, b)$。
    • $\delta_A(q_1, b)=q_1 \implies q_1 \in \delta_B(q_1, b)$。所以 $\delta_B(q_1, b) = \{q_0, q_1\}$。(图中画的箭头是从 $q_1$ 到 $q_0$ 和 $q_1$)
    • 图示画错了! 解答中的图示与它自己描述的构造规则不完全一致。这是一个常见的讲义错误。根据构造规则,从 $q_0$ 输入 a 应该能到 $q_0$ 和 $q_1$。但图中只画了到 $q_1$。
    • 让我们忽略图中的小错误,用正确的逻辑来验证: 识别 $L^R = b(a|b)^*$ 的 NFA 应该是什么样的?一个简单的 NFA 是 $s_0 \xrightarrow{b} s_1$,其中 $s_1$ 是接受状态且有 (a,b) 自循环。我们的构造是否等价于这个?
    • $q_0' \xrightarrow{\epsilon} q_1$。
    • $q_1 \xrightarrow{b} \{q_0, q_1\}$。
    • $q_0$ 是接受状态。所以读入 "b" 之后,一条路径到 $q_0$,一条到 $q_1$。因为到达了接受状态 $q_0$,所以 "b" 被接受。
    • 这说明构造本身是正确的,但图示可能为了简化而省略了一些细节,或者有误。
💡 [数值示例]
  • 语言 $L = \{w \mid w \text{以 } ab \text{ 结尾}\}$
  • DFA A: $q_0 \xrightarrow{a,b} q_0 \xrightarrow{a} q_1 \xrightarrow{b} q_2$ (接受)。
  • 构造 $L^R = \{w \mid w \text{以 } ba \text{ 开头}\}$ 的 NFA B:
  1. 新起始状态 $s$。
  2. 新接受状态 $\{q_0\}$。
  3. $s \xrightarrow{\epsilon} q_2$ (因为 $q_2$ 是 $A$ 的唯一接受状态)。
  4. 反转箭头:
    • $q_1 \xrightarrow{b} q_2$ 反转为 $q_2 \xrightarrow{b} q_1$。
    • $q_0 \xrightarrow{a} q_1$ 反转为 $q_1 \xrightarrow{a} q_0$。
    • $q_0 \xrightarrow{a} q_0$ 反转为 $q_0 \xrightarrow{a} q_0$。
    • $q_0 \xrightarrow{b} q_0$ 反转为 $q_0 \xrightarrow{b} q_0$。
    • 验证字符串 "ba": 路径 $s \xrightarrow{\epsilon} q_2 \xrightarrow{b} q_1 \xrightarrow{a} q_0$。停在接受状态 $q_0$。接受。
    • 验证字符串 "b": 路径 $s \xrightarrow{\epsilon} q_2 \xrightarrow{b} q_1$。停在非接受状态 $q_1$。拒绝。
⚠️ [易错点]
  1. DFA 与 NFA 的转换: 即使从一个 DFA 开始,逆向构造通常会产生一个 NFA。因为原 DFA 中可能有多个转移指向同一个状态,反转后就变成一个状态对同一输入有多个转移
  2. 多个接受状态的处理: 这是最关键的易错点。如果原 DFA 有多个接受状态,必须通过一个新的起始状态和多条 $\epsilon$-转移来处理,绝不能创建多个起始状态
  3. 对 $\epsilon$ 的处理: 如果原语言 $L$ 包含 $\epsilon$,那么其起始状态 $q_0$ 也是接受状态。在构造 $L^R$ 时,$q_0$ 既是 $B$ 的接受状态,也(作为原接受状态之一)是新起始状态 $q_0'$ 通过 $\epsilon$-转移指向的目标之一。
📝 [总结]

通过“反转箭头”并妥善处理“起始”和“接受”状态的转换,我们可以从一个识别 $L$ 的自动机构造出一个识别 $L^R$ 的 NFA。这个构造性证明表明,正规语言逆转运算是封闭的。

🎯 [存在目的]

这个问题一方面展示了另一个重要的封闭性质(逆转),另一方面也提供了一个非常正式的证明框架,教导学生如何严谨地论证一个构造的正确性(双向蕴含的证明)。

🧠 [直觉心智模型]

想象你看了一场电影(自动机 $A$ 处理字符串 $w$)。

  1. 电影从开头(起始状态)放到结尾(接受状态)。
  2. 现在你想“倒着看”这部电影(处理 $w^R$)。
  3. 构造 $L^R$ 的自动机 $B$ 就是你的“倒放”播放器。
  4. 你得从电影的结尾($A$ 的接受状态)开始播放。但一部电影可能有多个结局(多个接受状态),所以你的播放器需要一个“选择结局”的菜单(新的起始状态和 $\epsilon$-转移)。
  5. 然后你按下了倒放键,所有情节都反向发生(箭头反转)。
  6. 如果你最终能“倒放”到电影的开头($A$ 的起始状态),那么这次“倒看”就是成功的($w^R$ 被接受)。
💭 [直观想象]

想象一条单行道的公路网(DFA $A$)。

  1. 你从唯一的入口(起始状态)开车,希望到达某个或某几个出口(接受状态)。
  2. 现在政府决定,把所有路牌都掉个头,让大家反向行驶(构造 NFA $B$)。
  3. 原来的所有出口都变成了入口。为了方便管理,政府在所有旧出口前修了一个巨大的环岛(新起始状态 $q_0'$),并修了匝道($\epsilon$-转移)连接到这些旧出口。现在所有车都从这个大环岛进入。
  4. 原来的唯一入口,现在变成了唯一的终点收费站(新接受状态)。
  5. 如果你能从这个大环岛出发,沿着反向的路网,最终开到终点收费站,你就完成了一次合法的逆向旅程。

📜 [原文14]

现在我们证明 NFA $B$ 识别 $L^{R}$。为了证明这一点,我们必须证明:

$$ w^{R} \in L^{R} \Longleftrightarrow \text { NFA } B \text { 接受 } w^{R} $$

正向:$w^{R} \in L^{R} \Longrightarrow$ NFA $B$ 接受 $w^{R}$。

令 $w=a_{1}, a_{2} \ldots, a_{n} \in L, n \geq 0$ 且 $w^{R}=a^{n}, \ldots, a_{2}, a_{1} \in L^{R}$。由于 $w \in L$,对于所有 $a_{1}, \ldots, a_{n}$,存在某些 $q_{i}, r_{i} \in Q_{A}$ 使得 $\delta_{A}\left(q_{i}, a_{i+1}\right)=r_{i+1}, 0 \leq i \leq n-1$,其中 $q_{0}$ 是 $A$ 的起始状态且 $r_{n} \in F_{A}$。根据我们对 NFA $B$ 的定义,$r_{n} \in F_{A}$ 意味着存在一个起始状态 $q_{0}^{\prime} \in Q_{B}$ 使得 $\delta\left(q_{0}, \epsilon\right)=r$。再次根据我们对 $B$ 的定义,对于所有 $\delta_{A}\left(q_{i}, a_{i+1}\right)=r_{i+1}, 0 \leq i \leq n-1$,存在 $\delta_{B}\left(r_{i+1}, a_{i+1}\right)=q_{i} \in Q_{B}$。最后,由于 $F_{B}=\left\{q_{0}\right\}$,且存在 $\delta_{B}\left(r_{1}, a_{1}\right)=q_{0}$,字符串 $w^{R}=a_{n}, \ldots, a_{2}, a_{1} \in L^{R}$ 在 NFA $B$ 中有一条从其起始状态 $q_{0}^{\prime}$ 到其接受状态 $q_{0}$ 的接受计算

反向:NFA $B$ 接受 $w^{R} \Longrightarrow w^{R} \in L^{R}$

这与正向类似,但我们论证由于 NFA $B$ 接受 $w^{R}$,存在一条从 $q_{0}^{\prime} \in Q_{B}$ 到 $F_{B}=\{$起始状态 $q_{0} \in Q_{A}\}$ 的接受计算路径。(此处非正式描述)根据我们对 NFA $B$ 的构造,那么在 NFA $A$ 中对于字符串 $w=a_{1} a_{2} \ldots a_{n}$ 必须存在一条从 $q_{0} \in Q_{A}$ 到 $q_{n} \in F_{A}$ 的计算路径,因此 $w \in L=L(A)$。如果 $w \in L$,那么根据 $L^{R}$ 的定义,$w^{R} \in L^{R}$。我们证明了如果 $B$ 接受 $w^{R}$,那么 $w^{R} \in L^{R}$。

NFA $B$ 识别 $L^{R}$,因此 $L^{R}$ 是正规的。

📖 [逐步解释]

这部分是上一个构造的严格数学证明,旨在展示如何形式化地论证一个构造的正确性。证明一个自动机 $B$ 识别一个语言 $L_B$ 的标准方法是证明一个双向蕴含关系:一个字符串 $w$ 属于语言 $L_B$ 当且仅当自动机 $B$ 接受字符串 $w$。

证明目标: $w^R \in L^R \iff B \text{ 接受 } w^R$。

第一部分:正向证明 ($\Longrightarrow$)

  • 前提: 假设 $w^R \in L^R$。
  • 目标: 证明 NFA $B$ 接受 $w^R$。
  • 推导链:
  1. 如果 $w^R \in L^R$,根据逆转定义,那么原字符串 $w \in L$。
  2. 因为 DFA $A$ 识别 $L$,所以存在一条在 $A$ 中接受 $w$ 的计算路径。设 $w = a_1a_2...a_n$。这条路径可以写成状态序列:$s_0, s_1, ..., s_n$,其中 $s_0=q_0$ (A的起始),$s_n \in F_A$ (A的接受状态集),并且对于每一步 $i$ (从 0 到 n-1),都有 $\delta_A(s_i, a_{i+1}) = s_{i+1}$。
  3. 现在我们要在 NFA $B$ 中为 $w^R = a_n...a_2a_1$ 构造一条接受路径。
  4. $B$ 的计算从其起始状态 $q_0'$ 开始。
  5. 根据构造,从 $q_0'$ 有一条 $\epsilon$-转移到所有 $A$ 的接受状态。因为 $s_n \in F_A$,所以 $B$ 可以从 $q_0'$ 经过 $\epsilon$-转移到达状态 $s_n$。
  6. 接下来,$B$ 需要消耗 $w^R$ 的第一个字符 $a_n$。在 $A$ 中,我们有 $\delta_A(s_{n-1}, a_n) = s_n$。根据 $B$ 的构造(箭头反转),在 $B$ 中必然存在一个转移,使得从状态 $s_n$ 读入 $a_n$ 可以到达状态 $s_{n-1}$。
  7. 依此类推,对于 $w^R$ 的每一个字符 $a_i$ (从 $n$ 到 1),在 $B$ 中都存在一条路径 $s_i \xrightarrow{a_i} s_{i-1}$。
  8. 这样,我们就在 $B$ 中构建了一条完整的计算路径:$q_0' \xrightarrow{\epsilon} s_n \xrightarrow{a_n} s_{n-1} \xrightarrow{a_{n-1}} \dots \xrightarrow{a_2} s_1 \xrightarrow{a_1} s_0$。
  9. 这条路径的终点是 $s_0$。根据构造,$s_0$ (即 $A$ 的原起始状态) 正是 $B$ 的接受状态
  10. 结论: 我们成功地为 $w^R$ 在 $B$ 中找到了一条接受路径。因此,$B$ 接受 $w^R$。

第二部分:反向证明 ($\Longleftarrow$)

  • 前提: 假设 NFA $B$ 接受 $w^R$。
  • 目标: 证明 $w^R \in L^R$。
  • 推导链:
  1. 如果 $B$ 接受 $w^R = a_n...a_1$,那么在 $B$ 中一定存在一条从起始状态 $q_0'$ 到接受状态 $q_0$ (即 $F_B$) 的路径。
  2. 这条路径必然以 $q_0' \xrightarrow{\epsilon} s_n$ 开始,其中 $s_n$ 是 $A$ 的一个接受状态 ($s_n \in F_A$)。
  3. 然后,路径必须是 $s_n \xrightarrow{a_n} s_{n-1} \dots \xrightarrow{a_1} s_0$,其中终点 $s_0$ 必须是 $B$ 的接受状态,也就是 $A$ 的原起始状态
  4. 由于 $B$ 的转移是 $A$ 的转移的逆向,那么对于 $B$ 中的每一步 $s_i \xrightarrow{a_i} s_{i-1}$,在 $A$ 中都必然存在一步 $\delta_A(s_{i-1}, a_i) = s_i$。
  5. 将 $B$ 的整个计算路径反过来看,我们在 $A$ 中就得到了一条路径:$s_0 \xrightarrow{a_1} s_1 \xrightarrow{a_2} \dots \xrightarrow{a_n} s_n$。
  6. 这条路径在 $A$ 中,从起始状态 $s_0$ 开始,到接受状态 $s_n$ 结束,消耗的字符串是 $w = a_1a_2...a_n$。
  7. 因此,$A$ 接受字符串 $w$,即 $w \in L$。
  8. 根据逆转的定义,如果 $w \in L$,那么 $w^R \in L^R$。
  9. 结论: 我们证明了如果 $B$ 接受 $w^R$,那么 $w^R$ 必然属于语言 $L^R$。

最终结论:

由于双向蕴含关系都已证明,所以我们得出结论:NFA $B$ 识别的语言恰好是 $L^R$。因为我们能够为任何正规语言 $L$ 构造出这样一个识别 $L^R$ 的 NFA,所以 $L^R$ 也必然是正规语言正规语言集合在逆转运算下是封闭的。

∑ [公式拆解]
  • $w^{R} \in L^{R} \Longleftrightarrow \text { NFA } B \text { 接受 } w^{R}$:
  • $w^R$: 字符串 $w$ 的逆序。
  • $L^R$: 语言 $L$ 的逆转,是 $L$ 中所有字符串的逆序组成的集合。
  • $\Longleftrightarrow$: "当且仅当",表示左右两边的命题互为充要条件,需要双向证明。
  • NFA B 接受 $w^R$: 存在一条从 B 的起始状态开始,消耗完 $w^R$ 后,最终停在 B 的某个接受状态的计算路径。
📝 [总结]

这部分内容提供了一个构造性证明的经典范例。它通过严谨的逻辑推理,一步步地将在 DFA $A$ 上的正向计算路径与在 NFA $B$ 上的逆向计算路径对应起来,从而证明了构造的正确性。

🎯 [存在目的]

这部分的存在目的不仅仅是为了证明逆转封闭性,更重要的是,它是一个教学示例,用来展示如何将直观的构造思想转化为形式化的、滴水不漏的数学证明。这对于培养计算机科学理论所必需的严谨思维至关重要。

🧠 [直觉心智模型]

这就像是在证明一个双向翻译器是完美的。

  1. 正向证明: 随便拿一句中文 ($w$),用你的翻译器 $A$ 翻译成英文(被 $A$ 接受)。然后,把这句英文倒过来说 ($w^R$),交给你的逆向翻译器 $B$。如果 $B$ 能准确地把倒装的英文翻译回倒装的中文(被 $B$ 接受),说明正向翻译没问题。
  2. 反向证明: 随便拿一句倒装的英文 ($w^R$),如果你的逆向翻译器 $B$ 能成功处理它(接受 $w^R$),然后你检查一下,发现它对应的正向英文 ($w$) 确实是一句合法的、能被正向翻译器 $A$ 处理的句子。那就说明逆向翻译也没问题。
  3. 只有双向都验证无误,才能说这个逆向翻译器 $B$ 是完美对应翻译器 $A$ 的。

3.4.2 问题 7b

📜 [原文15]

问题 7b。理解 $\min (L)$ 的作用很重要。当我们说 $w \in L$ 且 $w$ 的真前缀不在 $L$ 中时,$\min (L)$ 运算会丢弃所有其前缀也可被接受的已接受字符串。例如,如果 $q_{i}, q_{j} \in F_{A}$,且存在一个字符串 $w=x y \in L$,其中 $\delta_{A}^{*}\left(q_{0}, x\right)=q_{i}, \delta_{A}^{*}\left(q_{i}, x\right)=q_{j}$,那么 $w \notin \min (L)$。换句话说,来自接受状态的每个转移都将导致一个拒绝状态

对于一个 DFA $A=\left(Q_{A}, \Sigma, \delta_{A}, q_{0}, F_{A}\right)$ 使得 $L(A)=L$,我们将构造 DFA $B=\left(Q_{B}, \Sigma, \delta_{B}, q_{0}^{\prime}, F_{B}\right)$ 使得

  • $Q_{B}=Q_{A} \cup\left\{q_{\text{reject}}\right\}$,其中 $q_{\text{reject}}$ 是拒绝状态。我们需要这个额外的拒绝状态来捕获所有来自接受状态转移
  • $q_{0}^{\prime}=q_{0}$。$A$ 的起始状态与 $B$ 的起始状态相同。
  • $F_{B}=F_{A}$。$A$ 和 $B$ 具有相同的接受状态
  • 对于所有 $a \in \Sigma$ 和 $q \in F_{A}=F_{B}, \delta_{B}(q, a)=q_{\text{reject}}$。所有来自接受状态转移都将到达拒绝状态 $q_{\text{reject}}$。
  • 对于所有 $a \in \Sigma, \delta_{B}\left(q_{\text{reject}}, a\right)=q_{\text{reject}}$。一旦到达 $q_{\text{reject}}$,DFA状态就不再改变。
  • 对于所有 $a \in \Sigma$ 和 $q \notin F_{B} \cup q_{\text{reject}}, \delta_{B}(q, a)=\delta_{A}(q, a)$。来自所有其他状态转移保持不变。
📖 [逐步解释]

这个问题要求证明正规语言对一个名为 $\min$ 的运算是封闭的。首先我们需要理解 $\min(L)$ 的定义。

$\min(L)$ 的定义:

$\min(L) = \{w \in L \mid w \text{ 的任何真前缀都不在 } L \text{ 中}\}$。

一个字符串的真前缀是指该字符串的、不等于其自身的前缀。例如,"apple" 的真前缀有 $\epsilon$, "a", "ap", "app", "appl"。

$\min(L)$ 语言的本质是:保留 $L$ 中那些“最短”的、不可再缩短的被接受的字符串。如果一个字符串 $w$ 在 $L$ 中,但它的某个前缀 $x$ 也在 $L$ 中,那么 $w$ 就像是一个“多余”的、更长的版本,它会被 $\min(L)$ 丢弃。

构造思路:

我们要从一个识别 $L$ 的 DFA $A$ 出发,构造一个识别 $\min(L)$ 的 DFA $B$。

  • 根据 $\min(L)$ 的定义,如果一个字符串 $w=xy$ 被 $A$ 接受,并且它的前缀 $x$ 也被 $A$ 接受,那么 $w$ 不应该被 $B$ 接受。
  • DFA $A$ 中,这意味着什么?如果处理前缀 $x$ 后,DFA $A$ 到达了一个接受状态,那么之后再处理后续的字符串 $y$ 时,无论最终停在哪里,整个字符串 $w=xy$ 都不属于 $\min(L)$。
  • 这启发我们:一旦进入一个接受状态,就不能再“出来”继续进行有效的计算了。任何从接受状态出发的转移,都应该导向一个万劫不复的“失败”境地。

DFA $B$ 的构造步骤:

  1. 状态: 复制 $A$ 的所有状态,并增加一个全新的拒绝状态(或称“陷阱状态”、“死亡状态”),记为 $q_{reject}$。
  2. 起始状态: $B$ 的起始状态和 $A$ 的起始状态相同。
  3. 接受状态: $B$ 的接受状态集合和 $A$ 的接受状态集合相同。
  4. 修改转移:
    • 核心修改: 对于 $A$ 中所有接受状态 $q$,在 $B$ 中,从 $q$ 出发的所有转移(无论输入什么字符)全部指向我们新加的 $q_{reject}$。
    • 陷阱状态的自循环: $q_{reject}$ 对所有输入都有一个指向自身的转移。一旦进入,永不离开。
    • 其他转移不变: 对于 $A$ 中所有非接受状态 $q$,它们原来的转移在 $B$ 中保持不变。

工作原理:

  • DFA $B$ 处理一个字符串 $w$ 时,只要它还没有遇到一个属于 $L$ 的前缀,它的行为就和 DFA $A$ 完全一样。
  • 如果 $w$ 本身属于 $L$,并且没有任何真前缀属于 $L$,那么 $B$ 会和 $A$ 一样,在消耗完 $w$ 的最后一个字符时,恰好到达一个接受状态。因此,$w$ 被 $B$ 接受。
  • 如果 $w$ 有一个真前缀 $x$ 属于 $L$ (设 $w=xy, y$ 非空),那么当 $B$ 处理完前缀 $x$ 时,它会到达一个接受状态。但此时字符串还没结束,还需要处理 $y$。根据我们的新规则,从这个接受状态出发的第一步(即处理 $y$ 的第一个字符),就会立即让 $B$ 转移到 $q_{reject}$ 状态。之后无论 $y$ 剩下什么,B 都会一直停在 $q_{reject}$。最终字符串 $w$ 结束时,B 停在非接受的 $q_{reject}$ 状态,$w$ 被拒绝。

这个构造完美地实现了 $\min(L)$ 的定义。

💡 [数值示例]
  • 语言 $L = \{a^n b \mid n \ge 0\}$ (任意多个 a 后面跟一个 b)。
  • DFA A: $q_0 \xrightarrow{a} q_0$, $q_0 \xrightarrow{b} q_1$ (接受)。$q_1$ 对 a,b 的转移可以指向一个死亡状态(或省略)。
  • 字符串 "ab" $\in L$,"aab" $\in L$。
  • 但 "a" 是 "ab" 的前缀,而 "a" $\notin L$。
  • "ab" 是 "abb" 的前缀,"ab" $\in L$。所以 "abb" $\notin \min(L)$。
  • 实际上,$\min(L) = L$。因为 $L$ 中没有一个字符串是另一个字符串的前缀。
  • 用构造法验证:
  1. $A$ 的接受状态是 $q_1$。
  2. 构造 $B$:将从 $q_1$ 出发的所有转移都指向 $q_{reject}$。
  3. 结果 $B$ 和 $A$ 的行为对于 $L$ 中的字符串完全一样,因为一旦到达 $q_1$,字符串就结束了。所以 $B$ 识别的语言还是 $L$。
  • 语言 $L = (01)^*$ (01 的任意次重复: $\{\epsilon, 01, 0101, \dots\}$)。
  • 字符串 "01" $\in L$。
  • 字符串 "0101" $\in L$。
  • "01" 是 "0101" 的真前缀。所以 "0101" $\notin \min(L)$。
  • 同理,所有长度大于 0 的字符串都会被剔除。
  • 只有 $\epsilon$ 没有真前缀。$\epsilon \in L$。所以 $\min(L) = \{\epsilon\}$。
  • 用构造法验证:
  1. DFA A: $q_0 \xrightarrow{0} q_1 \xrightarrow{1} q_0$。$q_0$ 是起始和接受状态。
  2. 构造 $B$:$A$ 的接受状态是 $\{q_0\}$。
  3. 修改转移: 从 $q_0$ 出发的所有转移都要指向 $q_{reject}$。
    • $\delta_B(q_0, 0) = q_{reject}$
    • $\delta_B(q_0, 1) = q_{reject}$
  4. $q_1$ 的转移不变: $\delta_B(q_1, 1) = q_0$。
  5. $B$ 的起始状态是 $q_0$,也是接受状态
  6. 分析 $B$:
    • 输入 $\epsilon$:停在 $q_0$,接受。
    • 输入 "0":$q_0 \xrightarrow{0} q_{reject}$,拒绝。
    • 输入任何非空字符串,第一步就会进入 $q_{reject}$。
    • 所以 $B$ 只接受 $\epsilon$。$L(B) = \{\epsilon\}$,与我们推导的 $\min(L)$ 一致。
⚠️ [易错点]
  1. DFA vs NFA: 这个构造是基于 DFA 的。如果起始自动机是 NFA,情况会复杂得多,因为 NFA 在同一步可能处于多个状态,其中一个可能是接受的,而另一个不是。
  2. 处理所有接受状态: 如果原 DFA 有多个接受状态,必须修改从所有这些接受状态出发的转移
  3. 新旧状态的区分: 构造的新 DFA $B$ 是一个全新的对象,虽然它借用了 $A$ 的大部分结构,但其转移函数 $\delta_B$ 已经与 $\delta_A$ 不同了。
📝 [总结]

我们通过一个构造性证明,展示了正规语言对 $\min$ 运算是封闭的。其核心思想是:修改原 DFA,使得任何计算路径一旦到达一个接受状态,就“锁死”在那里或进入一个陷阱状态,无法再继续匹配更长的字符串。这精确地移除了所有“非最小”的被接受字符串。

🎯 [存在目的]

这个问题引入了一个不那么常见但很有趣的语言运算,并要求为其封闭性提供构造证明。这能检验学生是否能深刻理解 DFA 的计算过程,并能根据一个新的语言定义,创造性地修改 DFA 的结构以实现新的识别逻辑。

🧠 [直觉心智模型]

这就像一个“见好就收”的赌徒。

  1. DFA A 是一个普通的赌徒,只要最终赢钱(停在接受状态),他就高兴。他可能会在赢了一局(到达一个接受状态)之后,继续玩下去,可能会赢更多,也可能会输光。
  2. DFA B (识别 $\min(L)$) 是一个非常谨慎的赌徒。他的规则是:一旦赢了钱(到达一个接受状态),必须马上收手回家(进入 $q_{reject}$ 状态)。如果他不收手,还想继续玩下一局,那他之前赢的钱就全部作废(最终停在 $q_{reject}$)。
  3. 所以,DFA B 只会认可那些“刚好在最后一刻才赢钱”的玩法。

📜 [原文16]

要证明这是正确的,我们需要证明:

$$ \begin{aligned} & w \in \min (L) \Longrightarrow \text { DFA } B \text { 接受 } w \\ & w \notin \min (L) \Longrightarrow \text { DFA } B \text { 拒绝 } w \end{aligned} $$

第一条陈述:$w \in \min (L) \Longrightarrow$ DFA $B$ 接受 $w$。

注意 $\min (L) \subset L$。如果 $w \in \min (L)$,则 $w \in L$,且在输入 $w$ 时,DFA $A$ 结束于一个接受状态 $q \in F_{A}=F_{B}$。其次,由于 $w \in \min (L)$,没有 $w$ 的真前缀在 $L$ 中。这意味着,对于任何满足 $w=x y,|y|>0$ 的 $x$,$x \notin L$。在输入 $x$ 时,DFA $A$ 将结束于一个状态 $q \notin F_{A}=F_{B}$。这意味着在计算 $w$ 时,DFA $A$ 和 $B$ 仅在最后一个字符时到达接受状态。因此 DFA $B$ 接受 $w$。

第二条陈述:$w \notin \min (L) \Longrightarrow$ DFA $B$ 拒绝 $w$。

如果 $w \notin \min (L)$,则 $w \notin L$ 或者存在 $w$ 的某些真前缀 $\in L$。

  • 如果 $w \notin L$,那么 $w$ 在 DFA $A$ 中将结束于一个非接受状态。由于 $F_{A}=F_{B}$,该状态DFA $B$ 中也是非接受的。$w$ 将被 DFA $B$ 拒绝。
  • 如果存在某些真前缀 $w \in L$,那么存在一个最短的真前缀 $x \in L$,其中 $w=x y,|y|>0$。那么 DFA $B$ 将拒绝 $w$,因为在看到 $x$ 之后的任何转移都将进入并保持在 $q_{\text{reject}}$。
📖 [逐步解释]

这部分是对 min(L) 构造的正确性进行形式化证明。证明分为两个部分,对应于 min(L) 定义的两个方面。

第一部分:证明 $w \in \min (L) \Longrightarrow B \text{ 接受 } w$

  • 前提: 假设 $w \in \min(L)$。
  • 目标: 证明 DFA $B$ 接受 $w$。
  • 推导链:
  1. 根据 $\min(L)$ 的定义,如果 $w \in \min(L)$,那么它必须满足两个条件:
    • (a) $w \in L$。
    • (b) $w$ 的任何真前缀 $x$ 都不在 $L$ 中。
  2. 从条件 (a) $w \in L$ 可知,DFA $A$ 在处理完 $w$ 后,会停在一个接受状态 $q_{final} \in F_A$。
  3. 从条件 (b) 可知,对于 $w$ 的任何真前缀 $x$,DFA $A$ 在处理完 $x$ 后,会停在一个非接受状态
  4. 现在来看 DFA $B$ 的行为。当 $B$ 处理 $w$ 时,对于 $w$ 的所有真前缀 $x$,计算路径所经过的状态都是非接受状态。根据我们的构造,在这些非接受状态上,$B$ 的转移行为与 $A$ 完全相同。
  5. 因此,在消耗完 $w$ 的最后一个字符之前,$B$ 的计算路径与 $A$ 的完全一样。
  6. 当最后一个字符被消耗时,$B$ 会和 $A$ 一样,到达状态 $q_{final}$。
  7. 因为 $F_B = F_A$,所以 $q_{final}$ 也是 $B$ 的一个接受状态
  8. 字符串处理结束,B 停在接受状态 $q_{final}$。
  9. 结论: DFA $B$ 接受 $w$。

第二部分:证明 $w \notin \min (L) \Longrightarrow B \text{ 拒绝 } w$

  • 前提: 假设 $w \notin \min(L)$。
  • 目标: 证明 DFA $B$ 拒绝 $w$。
  • 推导链:
  • $w \notin \min(L)$ 意味着以下两种情况之一为真:
  • 情况 1: $w$ 本身就不在 $L$ 中。
  1. 如果 $w \notin L$,那么 DFA $A$ 处理完 $w$ 后会停在一个非接受状态
  2. 我们来分析 $B$ 的行为。在处理 $w$ 的过程中,如果路径从未经过任何 $A$ 的接受状态,那么 $B$ 的行为和 $A$ 完全一样,最终也会停在同一个非接受状态,因此拒绝 $w$。
  3. 如果在处理 $w$ 的过程中,路径经过了某个 $A$ 的接受状态(即 $w$ 的某个前缀 $x \in L$),那么这就变成了下面的情况 2。
  4. 因此,只要我们能证明情况 2 会导致拒绝,情况 1 也就被覆盖了。
    • 情况 2: $w \in L$ 但它有一个真前缀 $x$ 也在 $L$ 中。
  5. 设 $w = xy$,其中 $x$ 是 $w$ 的一个真前缀(所以 $y$ 非空),并且 $x \in L$。
  6. 当 DFA $B$ 开始处理 $w$ 时,它首先处理前缀 $x$。
  7. 因为 $x \in L$,DFA $A$ 在处理完 $x$ 后会到达一个接受状态 $q_x \in F_A$。
  8. 由于在到达 $q_x$ 之前,$B$ 的行为和 $A$ 一样,所以 $B$ 在处理完 $x$ 后也会到达 $q_x$。
  9. 此时,字符串还没结束,B 需要继续处理 $y$ 的第一个字符,设为 $a$。
  10. 根据我们的构造,从接受状态 $q_x$ 出发,对于任何输入 $a$,B 的下一状态是 $\delta_B(q_x, a) = q_{reject}$。
  11. 一旦进入 $q_{reject}$ 状态,对于 $y$ 剩下的所有字符,B 都会一直停留在 $q_{reject}$。
  12. 字符串 $w$ 处理完毕后,B 最终停在状态 $q_{reject}$。
  13. $q_{reject}$ 不是接受状态
  14. 结论: DFA $B$ 拒绝 $w$。

最终结论:

两个方向的证明都已完成。我们证明了我们构造的 DFA $B$ 精确地识别了语言 $\min(L)$。因此,$\min(L)$ 是正规语言

📝 [总结]

这部分通过分情况讨论,严谨地证明了构造的 DFA $B$ 的正确性。证明的核心在于,DFA $B$ 的行为与 DFA $A$ 的差异点(即从接受状态出发的转移)恰好对应了 $\min(L)$ 语言定义中“没有真前缀在L中”这一关键约束。

🎯 [存在目的]

这部分的存在目的与上一节类似,都是为了展示如何进行形式化证明。它教会学生如何将一个语言的定义(包含逻辑“与”和“非”)分解成可以在自动机计算路径上追踪的条件,并通过分析这些条件下的自动机行为来完成证明。

44 正则表达式

4.1 问题 8

📜 [原文17]

问题 8. 用文字描述这些正则表达式表达的语言:答案。

(a) $0^{*} 1^{*}$

这是具有任意数量的 0,后跟任意数量的 1 的二进制字符串集合。

$\left\{0^{n} 1^{m} \mid n, m \geq 0\right\}$

📖 [逐步解释]
  • 正则表达式: $0^*1^*$
  • 拆解:
  • $0^*$: 这里的 *克莱尼星号,表示它前面的元素 0 可以出现 0 次或任意多次。所以 $0^*$ 代表的语言是 $\{\epsilon, 0, 00, 000, \dots\}$。
  • $1^*$: 同理,这代表 1 出现 0 次或任意多次。语言是 $\{\epsilon, 1, 11, 111, \dots\}$。
  • 连接: 整个表达式是 $0^*$ 连接 $1^*$。连接操作意味着先从 $0^*$ 代表的语言里取一个字符串,然后从 $1^*$ 代表的语言里取一个字符串,把它们拼在一起。
  • 语言描述:
  • 取 $w_0 \in 0^*$ 和 $w_1 \in 1^*$,则语言中的字符串形式为 $w_0w_1$。
  • 这意味着,字符串必须先由若干个(包括零个)'0' 组成,然后由若干个(包括零个)'1' 组成。
  • 一旦出现了一个 '1',后面就不能再出现 '0'。
  • 形式化描述: $\{0^n 1^m \mid n \ge 0, m \ge 0\}$。$n$ 和 $m$ 分别代表 0 和 1 出现的次数,它们可以独立地取任何非负整数。
💡 [数值示例]
  • n=2, m=3: "00111" (属于该语言)
  • n=1, m=0: "0" (属于该语言)
  • n=0, m=2: "11" (属于该语言)
  • n=0, m=0: $\epsilon$ (空字符串,属于该语言)
  • "010": 不属于该语言,因为在 '1' 出现之后又出现了 '0'。

📜 [原文18]

(b) $(01)^{*}$

这是具有重复模式 01 的二进制字符串集合。$\left\{(01)^{n} \mid n \geq 0\right\}$

📖 [逐步解释]
  • 正则表达式: $(01)^*$
  • 拆解:
  • (01): 括号用于组合。01 表示字符 '0' 后面紧跟着字符 '1'。这是一个单元。
  • $^*$: 克莱尼星号作用于括号里的整个单元 01
  • 语言描述:
  • 这意味着 01 这个单元可以重复 0 次或任意多次。
  • 形式化描述: $\{(01)^n \mid n \ge 0\}$。
💡 [数值示例]
  • n=0: $\epsilon$ (空字符串)
  • n=1: "01"
  • n=3: "010101"
  • "010": 不属于该语言,因为它不是 "01" 的整数次重复。
  • "10": 不属于该语言。

📜 [原文19]

(c) $\left(0^{*} 1^{*}\right)^{*}$

这是所有二进制字符串的集合。$L=\{0,1\}^{*}$

📖 [逐步解释]
  • 正则表达式: $(0^*1^*)^*$
  • 拆解:
  • $0^*1^*$: 我们已经知道,这代表“任意个 0 后面跟任意个 1”。例如 $\epsilon, 0, 1, 00, 11, 01, 001$ 都在这个语言里。
  • $()^*$: 最外层的星号表示,我们可以从括号里的语言 ($0^*1^*$) 中任意挑选字符串,然后把它们连接起来任意多次。
  • 语言描述:
  • 让我们看看能生成什么。
  • 只选一次,可选 "01"。
  • 只选一次,可选 "1"。
  • 只选一次,可选 "0"。
  • 选两次:第一次选 "1",第二次选 "0"。拼起来就是 "10"。
  • 既然我们可以生成 "0" 和 "1",并且可以任意次地将它们连接起来,那么我们就能生成由 "0" 和 "1" 构成的任何字符串。例如,要生成 "101",我们可以选三次:第一次选 "1",第二次选 "0",第三次选 "1"。
  • 结论: 这个正则表达式可以生成字母表 $\{0, 1\}$ 上的所有可能字符串。
  • 形式化描述: $\{0,1\}^*$。
💡 [数值示例]
  • 生成 "101":
  • 取 $(0^*1^*)$ 中的 "1" (n=0, m=1)。
  • 再取 $(0^*1^*)$ 中的 "0" (n=1, m=0)。
  • 再取 $(0^*1^*)$ 中的 "1" (n=0, m=1)。
  • 连接起来:"1" + "0" + "1" = "101"。
  • 生成 "00110":
  • 取 $(0^*1^*)$ 中的 "0011" (n=2, m=2)。
  • 再取 $(0^*1^*)$ 中的 "0" (n=1, m=0)。
  • 连接起来:"0011" + "0" = "00110"。

📜 [原文20]

(d) $(0 \cup 1)^{*}$

这也是所有二进制字符串的集合!思考为什么 c 部分的正则表达式与 d 部分表示相同的语言。

📖 [逐步解释]
  • 正则表达式: $(0 \cup 1)^*$
  • 拆解:
  • $0 \cup 1$: $\cup$ 符号代表并集或“或”。所以 0 $\cup$ 1 代表的语言是 {0, 1},即可以选 '0' 或者选 '1'。
  • $()^*$: 星号作用于括号内的语言 {0, 1}
  • 语言描述:
  • 这意味着我们可以从集合 {0, 1} 中任意挑选字符('0' 或 '1'),并重复这个过程任意多次(0 次或多次)。
  • 这正是“所有由 '0' 和 '1' 构成的字符串”的定义。
  • 形式化描述: $\{0,1\}^*$。

为什么 $(0^*1^*)^*$ 和 $(0 \cup 1)^*$ 等价?

  • $(0 \cup 1)^*$ 很明显是所有二进制字符串。我们需要证明 $(0^*1^*)^*$ 也能生成所有二进制字符串。
  • 证明的关键在于证明 $(0^*1^*)^*$ 可以生成任意单个的 '0' 和任意单个的 '1'。
  • 要生成 '0',我们可以在 $(0^*1^*)^*$ 中只重复一次,那次选择 $0^*1^*$ 中的 "0" (令 n=1, m=0)。
  • 要生成 '1',我们可以在 $(0^*1^*)^*$ 中只重复一次,那次选择 $0^*1^*$ 中的 "1" (令 n=0, m=1)。
  • 既然我们可以生成基本的构建块 '0' 和 '1',并且外层的星号允许我们将这些构建块任意次地连接起来,那么我们自然可以构建出任何由 '0' 和 '1' 组成的字符串。
  • 因此,$(0^*1^*)^*$ 描述的语言和 $(0 \cup 1)^*$ 描述的语言是相同的。

📜 [原文21]

(e) $0^{*}\left(10^{*} 1\right)^{*} 0^{*}$

这是所有具有偶数个 1 的二进制字符串集合。

📖 [逐步解释]
  • 正则表达式: $0^*(10^*1)^*0^*$
  • 拆解:
  • $0^*$ (开头): 字符串可以由任意多个(包括 0 个)'0' 开始。
  • $0^*$ (结尾): 字符串可以由任意多个(包括 0 个)'0' 结束。
  • (10\*1): 这是核心部分。它描述了什么?
  • 一个 '1'。
  • 后面跟着任意多个 '0' ($0^*$)。
  • 再后面跟着一个 '1'。
  • 所以,10*1 描述的是一对 '1',它们之间可以被任意多个 '0' 分隔。例如:"11", "101", "1001"。
  • $(10^*1)^*$: 星号作用于 10*1 这个单元。这意味着,这种“成对出现的 '1'”的模式可以重复 0 次或任意多次。
  • 重复 0 次:语言是 $\epsilon$。
  • 重复 1 次:语言是 $10^*1$。
  • 重复 2 次:语言是 $(10^*1)(10^*1)$,例如 "11101"。
  • 语言描述:
  • 把所有部分组合起来看:$0^* (10^*1)^* 0^*$。
  • 字符串的结构是:一堆 '0' (可选) + 一堆“成对的 '1' 模式” (可选) + 一堆 '0' (可选)。
  • 关键在于 '1' 的数量。'1' 只出现在 (10*1) 这个部分。而这个部分每次都提供两个 '1'。由于这个部分可以重复任意次 ($n \ge 0$),所以 '1' 的总数必然是 $2n$,即偶数
  • '0' 可以出现在开头、结尾,以及成对的 '1' 之间,所以 '0' 的数量和位置是任意的。
  • 结论: 这个正则表达式描述的是所有包含偶数个 '1' 的二进制字符串。
💡 [数值示例]
  • 字符串 "010100": 有两个 '1' (偶数),应该被接受。
  • 匹配 $0^*$ (开头): "0"。
  • 匹配 $(10^*1)^*$ 一次: "101"。
  • 匹配 $0^*$ (结尾): "00"。
  • 组合起来: "0" + "101" + "00" = "010100"。匹配成功。
  • 字符串 "111": 有三个 '1' (奇数),应该被拒绝。
  • $0^*$ (开头): $\epsilon$。
  • $(10^*1)^*$ 部分: 可以匹配 "11" (一次 101 中间 0 取 $\epsilon$)。剩下 "1"。
  • 剩下的 "1" 无法被结尾的 $0^*$ 匹配。
  • 或者,可以匹配 "1" ... 无法匹配 10*1
  • 无论如何都无法完全匹配。
  • 字符串 "000": 有零个 '1' (偶数),应该被接受。
  • 匹配 $0^*$ (开头): "000"。
  • 匹配 $(10^*1)^*$ 零次: $\epsilon$。
  • 匹配 $0^*$ (结尾): $\epsilon$。
  • 组合起来: "000" + $\epsilon$ + $\epsilon$ = "000"。匹配成功。

4.2 问题 9

📜 [原文22]

问题 9. 为以下语言构造正则表达式

(a) 这很简单。我们将 10 与该字母表上所有字符串的语言进行连接

答案:$10(0 \cup 1)^{*}$

📖 [逐步解释]
  • 语言: 所有以 "10" 开头的字符串。
  • 分析:
  • 一个字符串要满足这个条件,必须分为两部分:
  1. 开头部分: 必须是固定的 "10"。
  2. 结尾部分: 在 "10" 之后,可以是任何东西。
    • "任何东西" 在二进制字母表上,就是“任意由 0 和 1 组成的字符串”,其正则表达式是 $(0 \cup 1)^*$。
    • 构造:
    • 将这两部分连接起来即可。
    • 正则表达式: $10 (0 \cup 1)^*$。
💡 [数值示例]
  • 字符串 "1001": 匹配。开头是 "10",后面 "01" 属于 $(0 \cup 1)^*$。
  • 字符串 "10": 匹配。开头是 "10",后面是空字符串 $\epsilon$,它属于 $(0 \cup 1)^*$。
  • 字符串 "010": 不匹配,因为它不是以 "10" 开头。

📜 [原文23]

(b) 注意两个 0 的位置可以在被接受字符串中的任何位置。我们如何在正则表达式中表达这一点?想法是将两个 0 固定在一个位置,并在其周围填充 1,如下所示:$1^{*} 01^{*} 01^{*}$。这确保了在只有两个 0 的同时,任何位置都可以有任意数量的 1。

答案:10101*

📖 [逐步解释]
  • 语言: 恰好包含两个 '0' 的字符串。
  • 分析:
  • 整个字符串中必须有两个 '0',不多也不少。
  • '1' 可以出现任意多次,并且可以出现在任何位置:在第一个 '0' 之前,在两个 '0' 之间,以及在第二个 '0' 之后。
  • 构造:
  • 我们可以把结构想象成:(一些1) 0 (一些1) 0 (一些1)
  • "一些1" 或 "任意多个1" 的正则表达式是 $1^*$。
  • 将这个模式替换到结构中:$1^* 0 1^* 0 1^*$。
  • 验证:
  • $1^*$ (开头): 允许在第一个 '0' 之前有任意多个 '1'。
  • 0: 这是第一个必须出现的 '0'。
  • $1^*$ (中间): 允许在两个 '0' 之间有任意多个 '1'。
  • 0: 这是第二个必须出现的 '0'。
  • $1^*$ (结尾): 允许在第二个 '0' 之后有任意多个 '1'。
  • 这个结构保证了 '0' 恰好出现两次,而 '1' 可以自由地填充在这些 '0' 的周围。
💡 [数值示例]
  • 字符串 "10110": 匹配。
  • $1^*$ (开头) 匹配 "1"。
  • 第一个 '0' 匹配 '0'。
  • $1^*$ (中间) 匹配 "11"。
  • 第二个 '0' 匹配 '0'。
  • $1^*$ (结尾) 匹配 $\epsilon$。
  • 字符串 "00": 匹配。
  • $1^*$ (开头) 匹配 $\epsilon$。
  • 第一个 '0' 匹配 '0'。
  • $1^*$ (中间) 匹配 $\epsilon$。
  • 第二个 '0' 匹配 '0'。
  • $1^*$ (结尾) 匹配 $\epsilon$。
  • 字符串 "0111": 不匹配,因为它只有一个 '0'。
  • 字符串 "000": 不匹配,因为它有三个 '0'。

📜 [原文24]

(c) 奇数个 0 意味着有 $2n+1$ 个 0,其中 $n \geq 0$。所以我们可以在表达式中固定一个 0,并对两个 0 应用克莱尼星号运算来覆盖剩余的 $2n$。但仅仅说 $1^{*} 01^{*}(00 \cup 1)^{*}$ 是不够的。这迫使除第一个 0 之外的所有 0 都成对出现(例如 1010011),并且无法识别像 1010101 这样的字符串。相反,记住在前一个例子中我们已经展示了如何识别只有两个零(在任何位置)的字符串。我们可以简单地对其应用克莱尼星号来解释 $2n$ 个 0!然后在前面添加一个 $1^{*} 0$ 来解释额外的奇数 0。练习:将奇数 0 添加到前面还是后面有关系吗?为什么?

答案:$1^{*} 0\left(1^{*} 01^{*} 01^{*}\right)^{*}$

📖 [逐步解释]
  • 语言: 包含奇数个 '0' 的字符串。
  • 分析:
  • 奇数个 '0' 意味着至少有一个 '0',然后可以有任意多“成对的 '0'”。即 1 个,3 个,5 个...
  • 我们可以把问题分解:一个'0' + 偶数个'0'
  • '1' 可以在任何地方出现。
  • 构造:
  1. 偶数个 '0' 的模块:
    • 我们如何表达“偶数个 '0',中间可以任意插入 '1'”?
    • 一个想法是,'0' 总是成对出现的。从 (b) 小题,我们知道 $1^*01^*01^*$ 代表恰好两个 '0',周围是任意 '1'。
    • 我们可以把这个 (1^01^01^*) 作为一个“产生一对 '0'”的单元。
    • 对其应用克莱尼星号 (1^01^01^)^,就代表了这个单元可以重复 0 次,1 次,2 次... 每次重复都增加两个 '0'。所以这部分就代表了“偶数个'0',并且'1'可以任意分布”。
  2. 奇数个 '0':
    • 我们只需要在上述“偶数个'0'”的模式基础上,再增加一个 '0' 即可。
    • 这个额外的 '0' 可以放在哪里?它可以放在最前面。
    • 我们需要一个 '0',它前面可以有任意个 '1'。这部分是 $1^*0$。
    • 将这两部分连接起来:$1^*0$ (产生第一个'0') 连接 $(1^*01^*01^*)^*$ (产生后续成对的'0')。
    • 最终表达式: $1^*0(1^*01^*01^*)^*$
    • 解答练习: 将奇数 '0' 添加到前面还是后面有关系吗?
    • 表达式是 $1^*0 \text{ (偶数模块) } 1^*$ (因为最后的 '0' 后面也可能有 '1')。
    • 如果我们把单个 '0' 放到后面,比如 (偶数模块) 10 1,是否等价?
    • 偶数模块(1^01^01^)^
    • 前面:$1^*0(1^*01^*01^*)^*$。让我们考虑它能否生成 "01010"。
    • $1^*0$ 匹配第一个 "0"。
    • $(1^*01^*01^*)$ 匹配 "1010"。其中,第一个 $1^*$ 匹配 "1",第一个 $0$ 匹配 "0",第二个 $1^*$ 匹配 "1",第二个 $0$ 匹配 "0",第三个 $1^*$ 匹配 $\epsilon$。
    • 所以可以生成。
    • 后面:$(1^*01^*01^*)^* 1^*01^*$。让我们考虑能否生成 "01010"。
    • $(1^*01^*01^*)$ 匹配 "010"。这是不可能的,因为它需要两个 '0'。
    • 也许 $(1^*01^*01^*)$ 匹配 "01"?不可能。
    • 也许 $(1^*01^*01^*)$ 重复 0 次,即 $\epsilon$。那么表达式剩下 $1^*01^*$。这只能匹配含有一个 '0' 的字符串。
    • 这说明简单的把 $1^*0$ 放在后面是不行的。
    • 正确的思考方式: 整个字符串由任意多个 '1' 和奇数个 '0' 组成。
    • $1^*0(1^*01^*01^*)^*$ 的结构是 (任意1) (第一个0) (任意1和偶数个0)
    • 等价的结构也可以是 (任意1和偶数个0) (最后一个0) (任意1)
    • 所以 (1^01^01^)^ 1^0 1^ 也是可以的。
    • 答案给出的 10(10101) 是不完整的,因为它没有考虑最后一个'0'后面可能还有'1'。一个更完整的答案应该是 $1^*0(1^*01^*01^*)^*1^*$,或者 $1^*(1^*01^*01^*)^*01^*$。不过通常为了简化,末尾的 $1^*$ 如果可以被并入前面的星号表达式,可能会被省略。在 (10101)* 中,最后一个 $1^*$ 已经包含了处理结尾的 $1$ 的能力。所以答案 $1^{*} 0\left(1^{*} 01^{*} 01^{*}\right)^{*}$ 是正确的。

📜 [原文25]

(d) 这是一个具有挑战性的题目。如果你无法独立解决,不要担心!这里的想法是观察语言并看看可以利用什么模式。

令 $L$ 为所讨论的语言。$\{a, b, c\}$ 上所有字符串的语言的正则表达式是 $(a \cup b \cup c)^{*}$。我观察到,对于 $L$ 中出现的任何 $b$,我可以通过在 $b$ 之前添加 $c$、在 $b$ 之后添加 $a$、或者在 $b$ 之前和/或之后添加另一个 $b$ 来防止 $abc$ 的形成。这会导致像 $\ldots abc \ldots$ 这样的子串变成 $\ldots acbc \ldots, \ldots abac \ldots, \ldots abbc \ldots$ 或 $\ldots abbbc \ldots$。我可以通过 $(a \cup cb \cup ba \cup bb \cup bbb \cup c)^{*}$ 来做到这一点。注意 $(bb \cup bbb)^{*}$ 与 $\{b^{n} \mid n>=2\}$ 相同。这解释了合法的子串,如 $a b^{2n+1} c$,其中 $n>=1$。现在如果我们回头检查,我们无法构造任何带有子串 $a b c$ 的字符串。但我们还没有完成。通过强迫 $b$ 后面跟着 $b$ 或 $a$,或者前面跟着 $b$ 或 $c$,我们遗漏了可以以 $b$ 开头或结尾的好字符串,无论它前面是什么。例如,$bc$ 或 $ab$。为了解决这个问题,我们在开头和结尾添加 $(\epsilon \cup b)$ 以允许在那里添加 $b$。

答案:$(\epsilon \cup b)(a \cup cb \cup ba \cup bb \cup bbb \cup c)^{*}(\epsilon \cup b)$

📖 [逐步解释]
  • 语言: 不包含子串 "abc" 的所有由 {a, b, c} 组成的字符串。
  • 分析 (这是一个难题,通常用补集思想更容易):
  • 直接构造“不包含”的正则表达式通常非常困难。
  • 一个更系统的方法是:
  1. 构造识别“包含”子串 "abc" 的语言的正则表达式。这个很简单:$(a \cup b \cup c)^* abc (a \cup b \cup c)^*$。
  2. 将这个正则表达式转换为 NFA,再转换为 DFA。
  3. 将这个 DFA 的接受状态非接受状态对调,得到一个识别补集语言(即“不包含 a-b-c”)的 DFA。
  4. 再将这个新的 DFA 转换回正则表达式
    • 这个过程非常繁琐。解答中提供的是一种直接构造的思路,虽然巧妙但非常难以想到。
  • 解答中的直接构造思路分析:
  • 核心思想: 不要生成 "abc"。这意味着,每当出现一个 'a' 时,如果后面要跟 'b',那么再后面就不能跟 'c'。
  • 或者反过来看,每当出现一个 'b',我们需要检查它的“邻居”。
  • 如果 'b' 的前面是 'a',那它的后面就不能是 'c'。
  • 如果 'b' 的后面是 'c',那它的前面就不能是 'a'。
  • 解答的思路是,把所有“安全”的构建块列出来,然后用星号把它们串起来。
  • 安全的块:
  • a: 安全。
  • c: 安全。
  • b: 不一定安全,取决于上下文。
  • 如何让 b 变得安全?
  • 如果 b 前面是 c -> cb 是安全的,因为 cb 后面跟任何东西都不会构成 "abc"。
  • 如果 b 后面是 a -> ba 是安全的。
  • 如果 b 后面是 b -> bb 是安全的。
  • ...
  • 解答将所有可能的安全组合起来:a, c, cb, ba, bb, bbb 等。
  • a
  • c
  • ba ('b' 后面跟了个 'a')
  • cb ('b' 前面跟了个 'c')
  • bb (b 后面跟了个 b)
  • bbb
  • 把这些安全的“原子”组合起来: (a \cup c \cup ba \cup cb \cup bb \cup bbb ...)^*
  • 处理b的边界情况:
  • 上面的构造忽略了 b 可以出现在字符串的开头或结尾。例如,字符串 "b" 是合法的,"bc" 是合法的,"ab" 是合法的。
  • (a \cup cb \cup ...)^* 的构造中,b 总是被 ac 或其他 b "包裹"着。
  • 为了允许 b 单独出现在开头或结尾,解答在前后加上了 (\epsilon \cup b)
  • (\epsilon \cup b) 表示:开头可以有一个 b,也可以没有。结尾也可以有一个 b,也可以没有。
  • 最终表达式: $(\epsilon \cup b)(a \cup cb \cup ba \cup bb \cup bbb \cup c)^{*}(\epsilon \cup b)$。
  • 这个表达式非常复杂,而且 (bb \cup bbb \cup c) 似乎有误,应该是 (bb \cup bbb)c 分开。正确的应该是 (a \cup c \cup ba \cup cb \cup b^+)^*,其中 $b^+$ 表示一个或多个b。解答中 bb \cup bbb 只是 $b^n, n\ge2$ 的一个不完整尝试。
  • 这是一个非常tricky的题目,其解答的思路也较为晦涩和可能存在不严谨之处,理解其尝试规避"abc"的核心思想即可。

4.3 问题 10

📜 [原文26]

问题 10. 将以下 NFA 转换为 GNFA。然后通过消除状态GNFA 转换为正则表达式(我们注意到以不同顺序消除状态会导致不同但等价的正则表达式)。

📖 [逐步解释]

这个问题要求我们使用“通过 GNFA 消除状态”的方法,将一个 NFA 转换为等价的正则表达式。这是一个标准的算法。

算法步骤:

  1. 转换为 GNFA 格式:
    • a. 添加一个新的起始状态 $s_{start}$,并从它画一条 $\epsilon$-转移到原 NFA 的起始状态
    • b. 添加一个新的唯一接受状态 $s_{accept}$,并从所有原 NFA 的接受状态画 $\epsilon$-转移到 $s_{accept}$。
    • c. 如果两个状态之间有多条边,用并集 $\cup$ 合并它们。
    • d. 如果两个状态之间没有边,可以认为有一条用 $\emptyset$ 标记的边。
  2. 依次消除状态:
    • 选择一个非 $s_{start}$ 和 $s_{accept}$ 的中间状态 $q_{rip}$ 进行消除。
    • 对于每一对状态 $(q_i, q_j)$($q_i$ 可以是 $s_{start}$,$q_j$ 可以是 $s_{accept}$),我们需要更新从 $q_i$ 到 $q_j$ 的路径,以包含原来“经过” $q_{rip}$ 的路径。
    • 更新公式:$R_{new}(i, j) = R_{old}(i, j) \cup R(i, rip) (R(rip, rip))^* R(rip, j)$。
    • $R(i, j)$ 是从 $q_i$ 到 $q_j$ 的正则表达式标签。
    • 这个公式的含义是:从 $i$ 到 $j$ 的新路径 = 原来从 $i$ 直达 $j$ 的路径 (从 $i$ 到 $rip$ 的路径,接着在 $rip$ 自身循环任意次,再接着从 $rip$ 到 $j$ 的路径)。
  3. 重复步骤 2,直到只剩下 $s_{start}$ 和 $s_{accept}$。此时,从 $s_{start}$ 到 $s_{accept}$ 的边上的正则表达式就是最终答案。

📜 [原文27]

步骤 1:构造 GNFA

回想一下,GNFA 只是一个其转移箭头可以用任何正则表达式作为标签(而不仅仅是 $a \in \Sigma$ 或 $\epsilon$)的 NFA。给定一个任意的 DFANFA,按如下方式将其转换为 GNFA

- 新起始状态 $s$,具有到原始状态的 $\epsilon$-转移

- 新接受状态 $f$,这将是唯一的接受状态。添加从旧接受状态到 $f$ 的 $\epsilon$-转移

- 用标有符号并集转移替换标有多个符号的转移

- 所有缺失的转移都标为 $\emptyset$。为了清晰起见,我们在此省略。

📖 [逐步解释]
  • 原始 NFA:
  • 起始状态: $q_0$
  • 接受状态: $\{q_0\}$
  • 状态: $\{q_0, q_1, q_2\}$
  • 转换为 GNFA 格式:
  1. 新起始状态 $s$: 添加 $s$,并画一条 $\epsilon$-转移到原起始状态 $q_0$。
  2. 新接受状态 $f$: 添加 $f$。原接受状态是 $q_0$,所以从 $q_0$ 画一条 $\epsilon$-转移到 $f$。
  3. 合并转移:
    • 从 $q_2$ 到 $q_1$ 有两条边,分别标 '0' 和 '1'。合并为一条边,标签为 $0 \cup 1$。
  4. $\epsilon$ 替换为正则表达式: $\epsilon$ 是一个合法的正则表达式
  5. 缺失的边: 比如从 $q_1$ 到 $q_1$ 没有边,可以认为标签是 $\emptyset$。
    • 得到的 GNFA 如图所示,它有 5 个状态 {$s, q_0, q_1, q_2, f$},并准备好进行状态消除。

📜 [原文28]

步骤 2:移除 $q_{1}$

当我们从 GNFA 中移除一个状态时,我们必须考虑进入和离开该状态的所有转移。因此,考虑所有边对 ( $q \rightarrow q_{1}, q_{1} \rightarrow q^{\prime}$ ),其中 $q, q^{\prime} \neq q_{1}$

- $q_{2} \rightarrow q_{1}, q_{1} \rightarrow q_{0}:(0 \cup 1) 1 \equiv 01 \cup 11$

- $q_{2} \rightarrow q_{1}, q_{1} \rightarrow q_{2}:(0 \cup 1) 0 \equiv 00 \cup 10$

📖 [逐步解释]
  • 选择要消除的状态: $q_1$。
  • 识别相关的路径: 我们需要找到所有“经过” $q_1$ 的路径,即形如 $q_i \to q_1 \to q_j$ 的路径。
  • $q_1$ 的入边: 从 $q_2$ 来,标签是 $0 \cup 1$。
  • $q_1$ 的出边: 到 $q_0$ (标签 1),到 $q_2$ (标签 0)。
  • $q_1$ 没有自循环,所以 $R(1,1) = \emptyset$,那么 $(R(1,1))^* = \epsilon$。
  • 应用公式 $R_{new}(i, j) = R_{old}(i, j) \cup R(i, 1) (R(1, 1))^* R(1, j)$
  • 路径 $q_2 \to q_1 \to q_0$:
  • $i=2, j=0, rip=1$。
  • $R_{new}(2,0) = R_{old}(2,0) \cup R(2,1) (\epsilon) R(1,0)$
  • $R_{old}(2,0) = \emptyset$ (原来 $q_2$ 到 $q_0$ 没有直达边)。
  • $R(2,1) = 0 \cup 1$。
  • $R(1,0) = 1$。
  • 所以 $R_{new}(2,0) = \emptyset \cup (0 \cup 1) \cdot \epsilon \cdot 1 = (0 \cup 1)1 = 01 \cup 11$。
  • 于是在新图中,我们从 $q_2$ 到 $q_0$ 画一条标签为 $01 \cup 11$ 的边。
  • 路径 $q_2 \to q_1 \to q_2$:
  • $i=2, j=2, rip=1$。
  • $R_{new}(2,2) = R_{old}(2,2) \cup R(2,1) (\epsilon) R(1,2)$
  • $R_{old}(2,2) = \emptyset$ (原来 $q_2$ 没有自循环)。
  • $R(2,1) = 0 \cup 1$。
  • $R(1,2) = 0$。
  • 所以 $R_{new}(2,2) = \emptyset \cup (0 \cup 1) \cdot \epsilon \cdot 0 = (0 \cup 1)0 = 00 \cup 10$。
  • 于是在新图中,$q_2$ 获得了一个自循环,标签为 $00 \cup 10$。
  • 消除 $q_1$ 后,将这些新计算出的边画入图中,得到一个 4 状态的 GNFA。

📜 [原文29]

步骤 3:移除 $q_{2}$

- $s \rightarrow q_{0}, q_{0} \rightarrow f:\left(1 \cup 0(00 \cup 10)^{*}(01 \cup 11)\right)^{*} 0(00 \cup 10)^{*}$

- $q_{0} \rightarrow q_{2}, q_{2} \rightarrow q_{0}: 1 \cup 0(00 \cup 10)^{*}(01 \cup 11)$

📖 [逐步解释]
  • 选择要消除的状态: $q_2$。
  • 识别相关的路径:
  • $q_2$ 的入边: 从 $q_0$ 来,标签是 $0$。
  • $q_2$ 的出边: 到 $q_0$ (标签 $01 \cup 11$),到 $f$ (标签 $\epsilon$)。
  • $q_2$ 有自循环: 标签是 $00 \cup 10$。所以 $R(2,2) = 00 \cup 10$。
  • 应用公式更新路径:
  • 路径 $q_0 \to q_2 \to f$:
  • $i=0, j=f, rip=2$。
  • $R_{new}(0,f) = R_{old}(0,f) \cup R(0,2) (R(2,2))^* R(2,f)$
  • $R_{old}(0,f) = \epsilon$ (原来 $q_0$ 到 $f$ 的直达边)。
  • $R(0,2) = 0$。
  • $(R(2,2))^* = (00 \cup 10)^*$。
  • $R(2,f) = \epsilon$。
  • 所以 $R_{new}(0,f) = \epsilon \cup 0(00 \cup 10)^*\epsilon = \epsilon \cup 0(00 \cup 10)^*$。
  • 注意: 解答中似乎将这一步与下一步合并了,或者顺序不同。让我们严格按照图示的更新结果来反推。图中从 $q_0$ 到 $f$ 的新边是 $0(00 \cup 10)^*$。这说明它只计算了经过 $q_2$ 的那部分 $R(0,2)(R(2,2))^*R(2,f)$,并将其作为一条新的平行边添加,而不是与旧的 $\epsilon$ 边合并。这在算法执行中也是可以的,最后再合并平行边。
  • 路径 $q_0 \to q_2 \to q_0$:
  • $i=0, j=0, rip=2$。
  • $R_{new}(0,0) = R_{old}(0,0) \cup R(0,2) (R(2,2))^* R(2,0)$
  • $R_{old}(0,0) = 1$ (原来 $q_0$ 的自循环)。
  • $R(0,2) = 0$。
  • $(R(2,2))^* = (00 \cup 10)^*$。
  • $R(2,0) = 01 \cup 11$。
  • 所以 $R_{new}(0,0) = 1 \cup 0(00 \cup 10)^*(01 \cup 11)$。
  • 这更新了 $q_0$ 的自循环的标签。
  • 消除 $q_2$ 后,剩下 $s, q_0, f$ 三个状态。
  • $s \xrightarrow{\epsilon} q_0$。
  • $q_0$ 有一个自循环,标签为 $1 \cup 0(00 \cup 10)^*(01 \cup 11)$。
  • 从 $q_0$ 到 $f$ 有两条边:一条是原来的 $\epsilon$,一条是新加的 $0(00 \cup 10)^*$。我们可以将它们合并为 $\epsilon \cup 0(00 \cup 10)^*$。
  • 再次注意:解答的图中,从 $q_0$到 $f$ 只画了一条边,标签是 $0(00 \cup 10)^*$。它似乎在这里犯了一个错误,遗漏了原来从 $q_0$ 到 $f$ 的 $\epsilon$ 边。这个遗漏会影响最终结果。让我们暂时跟着它的错误计算下去。

📜 [原文30]

步骤 4:移除 $q_{0}$

- $s \rightarrow q_{0}, q_{0} \rightarrow f:\left(1 \cup 0(00 \cup 10)^{*}(01 \cup 11)\right)^{*} 0(00 \cup 10)^{*}$

从开始到结束状态正则表达式是 $\left(1 \cup 0(00 \cup 10)^{*}(01 \cup 11)\right)^{*} 0(00 \cup 10)^{*}$

📖 [逐步解释]
  • 选择要消除的状态: $q_0$。
  • 识别相关的路径: 只有一条路径需要计算,就是 $s \to q_0 \to f$。
  • 应用公式:
  • $i=s, j=f, rip=0$。
  • $R_{new}(s,f) = R_{old}(s,f) \cup R(s,0) (R(0,0))^* R(0,f)$。
  • $R_{old}(s,f) = \emptyset$ (s 和 f 原来不直连)。
  • $R(s,0) = \epsilon$。
  • $(R(0,0))^* = (1 \cup 0(00 \cup 10)^*(01 \cup 11))^*$。
  • $R(0,f) = 0(00 \cup 10)^*$ (这里我们使用了解答上一步中那条可能错误的边)。
  • 所以 $R_{new}(s,f) = \epsilon (1 \cup 0(00 \cup 10)^*(01 \cup 11))^* 0(00 \cup 10)^*$。
  • 最终表达式: $(1 \cup 0(00 \cup 10)^*(01 \cup 11))^* 0(00 \cup 10)^*$。

如果我们修正上一步的错误:

  • 如果上一步从 $q_0$ 到 $f$ 的边是 $\epsilon \cup 0(00 \cup 10)^*$。
  • 那么 $R(0,f) = \epsilon \cup 0(00 \cup 10)^*$。
  • 最终表达式会变成: $\epsilon (1 \cup 0(00 \cup 10)^*(01 \cup 11))^* (\epsilon \cup 0(00 \cup 10)^*)$。
  • 这个表达式更复杂,但它和解答给出的表达式描述的是不同的语言。解答的版本不接受 $\epsilon$,而修正后的版本接受 $\epsilon$。
  • 原始 NFA 的起始状态 $q_0$ 是接受状态,所以它接受 $\epsilon$。因此,正确的正则表达式必须能生成 $\epsilon$。
  • 结论: 解答在第 3 步中犯了一个错误,导致最终的正则表达式是错误的,它丢失了对 $\epsilon$ 和其他一些字符串的识别能力。
⚠️ [易错点]
  1. 消除顺序: 以不同的顺序消除状态,会得到不同形式但等价的正则表达式。但无论何种顺序,最终语言应该相同。
  2. 公式应用: 必须精确应用公式,特别是 $(R(rip,rip))^*$ 这一项,很容易忘记。
  3. 合并边: 在消除一步后,要记得将所有新产生的路径考虑到下一步的计算中。平行边要用 $\cup$ 合并。解答中的错误正是源于对平行边处理不当。
  4. $\epsilon$ 和 $\emptyset$: 在正则表达式代数中,$\epsilon$ 是单位元($R \epsilon = \epsilon R = R$),$\emptyset$ 是零元($R \emptyset = \emptyset R = \emptyset$, $R \cup \emptyset = R$)。要小心区分。

4.4 问题 11

📜 [原文31]

问题 11. 将 $(01)^{*}\left(10^{*} \cup 0\right)$ 转换为 NFA。我们不会尝试优化 NFA 或利用我们对语言的直觉,而是直接使用我们在课堂上看到的关于封闭性的构造,以便练习这些构造。运算顺序是 $()$,${ }^{*}$,$\circ$,$\cup$。因此,让我们适当地分解正则表达式

  1. 构建识别 (01)* 的 NFA

(a) 首先,构建识别 01 的 NFA

(b) 应用克莱尼星号得到识别 (01)* 的 NFA

  1. 构建识别 $\left(10^{*} \cup 0\right)$ 的 NFA

(a) 构建识别 $\left(10^{*}\right)$ 的 NFA

(b) 构建识别 0 的 NFA

(c) 应用并集运算得到识别 $\left(10^{*} \cup 0\right)$ 的 NFA

  1. 应用连接得到识别 $(01)^{*}\left(10^{*} \cup 0\right)$ 的 NFA

步骤 1a:构造识别 01 的 NFA

步骤 1b:应用克莱尼星号得到识别 (01)* 的 NFA

为了应用克莱尼星号运算,我们添加一个新起始状态,它通过 $\epsilon$-转移连接到旧的起始状态,并且接受状态通过 $\epsilon$-转移连接回起始状态 $s_{0}$。新的起始状态也是一个接受状态

步骤 2a:构建识别 (10*) 的 NFA

步骤 2b:构建识别 0 的 NFA

步骤 2c:应用并集运算得到识别 $\left(10^{*} \cup 0\right)$ 的 NFA

为了应用并集,我们添加一个额外的起始状态,它通过 epsilon 转移连接到所有旧的起始状态

步骤 3:应用连接得到识别 $(01)^{*}\left(10^{*} \cup 0\right)$ 的 NFA

为了应用连接,添加从接受状态 $s$ 和 $q_{2}$ 到状态 $s_{1}$ 的 $\epsilon$-转移。然后从新 NFA接受状态集中移除 $s_{0}$ 和 $q_{2}$。

上述 NFA 识别由 $(01)^{*}\left(10^{*} \cup 0\right)$ 生成的语言。

📖 [逐步解释]

这个问题要求我们将一个复杂的正则表达式通过标准的递归构造方法(汤普森构造法)转换为一个 NFA。这种方法不追求状态最少,但保证了过程的机械化和正确性。

正则表达式: $(01)^*(10^* \cup 0)$

分解与构造:

第一部分:构造识别 $(01)^*$ 的 NFA ($N_1$)

  • 1a. 构造 01: 这是连接
  • 构造 0: $s_0 \xrightarrow{0} s_1$ ($s_1$ 接受)。
  • 构造 1: $s_2 \xrightarrow{1} s_3$ ($s_3$ 接受)。
  • 连接它们:从 $s_1$ (0的接受状态) 画 $\epsilon$-转移到 $s_2$ (1的起始状态),并取消 $s_1$ 的接受状态。最终得到 $s_0 \xrightarrow{0} s_1 \xrightarrow{\epsilon} s_2 \xrightarrow{1} s_3$。可以简化为 $q_0 \xrightarrow{0} q_1 \xrightarrow{1} q_2$。
  • 1b. 构造 (01)^* (星号运算):
  • 取上面 01 的 NFA ($q_0 \to q_1 \to q_2$)。
  • 1. 添加新起始状态 $s_{new}$,并设为接受状态
  • 2. 添加 $\epsilon$-转移: $s_{new} \to q_0$ (进入循环)。
  • 3. 添加 $\epsilon$-转移: $q_2 \to s_{new}$ (完成一次并返回)。
  • 解答中的图示采用了另一种等价的星号构造方法:(1) 添加新起始/接受状态 $s$ (2) 添加 $s \xrightarrow{\epsilon} q_0$ (3) 添加 $q_2 \xrightarrow{\epsilon} s$ (4) 添加 $q_2 \xrightarrow{\epsilon} q_0$ 的返回边和 $s$ 成为接受状态。解答中的图更复杂,但核心思想都是为了实现“0次或多次重复”。

第二部分:构造识别 $(10^* \cup 0)$ 的 NFA ($N_2$)

  • 这是一个并集,所以我们先构造两部分,再合并。
  • 2a. 构造 10*:
  • 构造 1: $p_0 \xrightarrow{1} p_1$。
  • 构造 0*: (使用星号构造) $p_2 \xrightarrow{\epsilon} p_3 \xrightarrow{0} p_3 \xrightarrow{\epsilon} p_4$,$p_2,p_4$ 是接受状态。
  • 连接它们:$p_1$ (1的接受状态) $\epsilon$-转移到 $p_2$ (0*的起始状态)。$p_1$ 不再是接受状态。最终接受状态是 $p_4$。
  • 解答的图示简化了这个过程,直接画出:$p_0 \xrightarrow{1} p_1$,然后 $p_1$ 上有一个 '0' 的自循环。这在直觉上更简单,也是对 0* 的一种有效表达。所以得到 $p_0 \xrightarrow{1} p_1(\text{自循环}0)$,$p_1$ 是接受。
  • 2b. 构造 0: 很简单,$p_5 \xrightarrow{0} p_6$ ($p_6$ 接受)。
  • 2c. 构造 (10* \cup 0) (并集运算):
  • 取上面 10* 的 NFA 和 0 的 NFA。
  • 添加新起始状态 $s_{union}$。
  • 从 $s_{union}$ 分别画 $\epsilon$-转移到它们的原起始状态($p_0$ 和 $p_5$)。
  • 保留它们原来的接受状态($p_1$ 和 $p_6$)。

第三部分:构造最终 NFA (连接运算)

  • 我们现在有两个 NFA:$N_1$ 识别 $(01)^*$,$N_2$ 识别 $(10^* \cup 0)$。
  • 要构造 $N_1N_2$ 的 NFA。
  • 连接:
  1. 取 $N_1$ 的所有接受状态。在步骤 1b 的图中,是 $\{s, q_2\}$。
  2. 从这每一个接受状态画 $\epsilon$-转移到 $N_2$ 的起始状态 $s_{union}$。
  3. 取消 $N_1$ 的接受状态资格。它们现在是中转站。
  4. $N_1$ 的起始状态($s$)成为最终 NFA 的起始状态
  5. $N_2$ 的接受状态($\{p_1, p_6\}$)成为最终 NFA 的接受状态
    • 最终得到的那个巨大的、有很多 $\epsilon$-转移的图,就是严格按照构造规则生成的 NFA。
📝 [总结]

这个问题是封闭性构造的大集合,它系统地、自底向上地展示了如何将一个复杂的正则表达式分解成原子部分(如 '0', '1'),然后使用连接并集星号三种基本构造规则,像搭积木一样,一步步地搭建出最终的 NFA。

🎯 [存在目的]

这个问题的目的是让学生完整地演练一遍从正则表达式NFA 的标准转换算法(汤普森构造法)。这个算法是编译器理论中的一个基石,用于将用户写的模式(正则表达式)转换为机器可以执行的状态机。通过这个练习,学生能深刻理解该算法的递归和模块化本质。

🧠 [直觉心智模型]

这就像用一套标准的乐高积木来拼一个复杂的模型。

  1. 原子操作: a -> (start) --a--> (end)
  2. 并集: $R_1 \cup R_2$ -> (new_start) --e--> (start_1), (new_start) --e--> (start_2)
  3. 连接: $R_1R_2$ -> (end_1) --e--> (start_2)
  4. 星号: $R^*$ -> (new_start_end) --e--> (start), (end) --e--> (new_start_end)

无论给你的正则表达式多复杂,你都可以先把它像代数表达式一样拆解,然后严格按照这几条规则,一步步地把 NFA 模块拼接起来,最终完成整个模型。你不需要“聪明地”去思考怎么优化,只需要像机器人一样执行规则。

5行间公式索引

1. 双向蕴含关系:证明构造的正确性需要证明语言成员资格和自动机接受之间的双向充要条件。

$$ w^{R} \in L^{R} \Longleftrightarrow \text { NFA } B \text { 接受 } w^{R} $$

2. 证明正确性的两个方面:为了证明新构造的DFA B准确识别min(L),需要证明B接受所有属于min(L)的字符串,且拒绝所有不属于min(L)的字符串。

$$ \begin{aligned} & w \in \min (L) \Longrightarrow \text { DFA } B \text { 接受 } w \\ & w \notin \min (L) \Longrightarrow \text { DFA } B \text { 拒绝 } w \end{aligned} $$

65 讲义总结

5.1 讲义核心概念总结

📖 [逐步解释]

这份讲义的解答集全面地涵盖了计算理论中关于正规语言的几个核心支柱。通过一系列具体的问题和详尽的解答,它强化了以下关键概念:

  1. NFA 的设计与理解:我们学习了如何构造 NFA 来识别特定的语言。关键在于利用非确定性(一个状态在同一输入下可以有多个后续状态)和 $\epsilon$-转移(不消耗输入即可切换状态)来简化设计。例如,识别“包含子串 101”或满足“条件A或条件B”的语言时,NFA 的“猜测”和“并行宇宙”能力使其构造比等价的 DFA 更为直观和简洁。
  2. NFA 到 DFA 的转换:通过子集构造法,我们掌握了将任何 NFA(包括带 $\epsilon$-转移的)机械化地转换为一个等价 DFA 的标准算法。这个过程不仅是一个重要的实践技能,更是在理论上证明了 NFADFA 的表达能力是完全等价的——它们都只能识别正规语言,不多也不少。DFA 的每一个状态都对应着 NFA 可能处于的一个状态子集
  3. 正规语言的封闭性质:讲义通过构造性证明,生动地展示了正规语言在多种运算下是封闭的。这意味着对正规语言进行这些运算后,结果仍然是正规语言。我们实践了以下构造:
    • 并集 ($L_1 \cup L_2$): 创建一个新起始状态,通过 $\epsilon$-转移连接到两个旧自动机的起始状态
    • 连接 ($L_1 L_2$): 将第一个自动机的接受状态通过 $\epsilon$-转移连接到第二个自动机的起始状态
    • 克莱尼星号 ($L^*$): 创建一个既是起始状态又是接受状态的新枢纽状态,通过 $\epsilon$-转移实现进入、退出和循环。
    • 逆转 ($L^R$): 反转所有转移箭头,并巧妙地交换起始状态接受状态的角色。
    • 自定义运算 ($\min(L)$): 修改 DFA,使任何计算路径在第一次到达接受状态后便“锁死”,无法继续匹配更长的字符串。
  4. 正则表达式的诠释与构造:我们练习了如何将语言的文字描述翻译成正则表达式,以及反向地用自然语言描述一个给定的正则表达式。这需要将语言的性质分解为由连接并集 ($\cup$)星号 ($*$) 等基本部分组成的结构。
  5. 不同表示模型的等价性与转换:这是本讲义的核心。它通过两个方向的算法实践,强化了有限自动机正则表达式的等价性:
    • NFA 到正则表达式: 使用“通过 GNFA 消除状态”的方法,通过迭代地移除状态并更新边上的正则表达式,最终得到一个单一的表达式。
    • 正则表达式到 NFA: 使用“汤普森构造法”,自底向上地为正则表达式的每个部分(原子、并集、连接、星号)构建对应的 NFA 模块,然后像搭积木一样将它们拼接起来。

这些算法是计算理论的基石,并且在编译器设计、文本搜索工具(如 grep)、网络协议分析等领域有着广泛的实际应用。

📝 [总结]

本讲义解答是一份关于正规语言核心操作的实践手册。它通过解决一系列精心设计的问题,将抽象的理论知识(如封闭性、模型等价性)与具体的、可执行的算法步骤(如子集构造状态消除)紧密联系起来。贯穿始终的主线是正规语言的三种等价表示形式——DFANFA正则表达式——以及在它们之间自由转换的能力。通过亲手实践这些转换,学生能够真正内化理论,而不仅仅是停留在背诵定义的层面。

🎯 [存在目的]

这份文档的存在目的在于弥合理论教学与解决实际问题之间的鸿沟。它为学生提供了一个可以检验自己理解、学习标准解题范式、观察重要算法(如子集构造状态消除)的每一个机械化步骤的平台。特别是通过对克莱尼星号构造的“失败尝试”的分析,学生可以深刻理解正确构造方法中每个组件的必要性,从而避免常见错误。这不仅仅是答案的罗列,更是一种思维方式的训练。

🧠 [直觉心智模型]

您可以将这份讲义解答集看作是一本正规语言的“食谱书”。当你面对一个关于正规语言的问题时,你不必每次都从零开始冥思苦想。这本“食谱书”为你提供了经过验证的“配方”(即算法和标准构造方法),用于处理和转换不同形式的正规语言

  1. 需要识别“语言A 或 语言B”?请翻到“并集”这一节的食谱。
  2. 需要将一个复杂的、有多重路径的 NFA 转换为一个确定性的 DFA?请使用“子集构造”的配方。
  3. 需要从一个状态图得到其对应的正则表达式?“状态消除”的食谱会告诉你该怎么做。

这本食谱将你从一个凭直觉的“厨艺爱好者”,培养成一个能系统、稳定地解决问题的“专业厨师”。

💭 [直观想象]

想象你是一位专门研究“正则部落”的语言学家。这个部落的语言有三种书写系统(“方言”):

  1. 图画文字DFA/NFA 的状态图):直观、生动,但有时细节繁复。
  2. 符号代码正则表达式):简洁、强大,但有时难以直接看懂。
  3. 形式化描述(集合论语言定义):精确、无歧义,但最为抽象。

这份讲义就是你的高级翻译工作坊手册。它教你如何在这三种书写系统之间进行完美、无损的互译。它向你展示了如何获取一句复杂的“符号代码”(正则表达式),并将其含义准确地绘制成“图画文字”(NFA);或者如何观看一幅复杂的“图画”(NFA),并将其浓缩成一句精华的“符号代码”(正则表达式)。通过掌握这些翻译技巧,你才能真正成为一名精通“正则部落”语言的大师。

[[END_OF_RESPONSE]]所有解释内容已全部输出完毕。