📝 我的笔记

还没有笔记

选中页面文字后点击「高亮」按钮添加

2_上下文无关语言2.4.2.ZH解释

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

11. DPDAs 和 DCFGs 的关系

📜 [原文1]

在本节中,我们将证明 DPDADCFG 描述同一类带结束标记的语言。首先,我们将展示如何将 DCFG 转换为等价的 DPDA。这种转换在所有情况下都有效。其次,我们将展示如何进行反向转换,即将 DPDA 转换为等价的 DCFG。后一种转换仅适用于带结束标记的语言。我们将等价性限制在带结束标记的语言,因为如果没有此限制,这些模型是不等价的。我们之前已经证明结束标记不影响 DPDA 识别的语言类别,但它们确实影响 DCFG 生成的语言类别。没有结束标记DCFG 只生成 DCFL 的子类——那些无前缀的语言(参见问题 2.22)。请注意,每个带结束标记的语言都是无前缀的。

📖 [逐步解释]

本段是引言,为后续的理论证明设定了舞台和范围。它核心要传达的思想是:确定性下推自动机 (DPDA)确定性上下文无关文法 (DCFG) 这两种描述语言的工具,在某个特定条件下是等价的,这个条件就是语言必须是“带结束标记的”。

  1. 核心论点: 本节的目标是证明 DPDADCFG 在描述“带结束标记的语言”这个范畴内,能力是完全相同的。也就是说,如果一个带结束标记的语言可以用 DPDA 识别,那么它一定能用 DCFG 生成,反之亦然。
  2. 证明的两个方向: 为了证明两者等价,需要进行双向证明:
    • 方向一 (DCFG → DPDA): 证明任何一个 DCFG,我们都能构造出一个与之等价的 DPDA。这个方向的转换是普适的,即对任何 DCFG 都成立。
    • 方向二 (DPDA → DCFG): 证明任何一个识别“带结束标记语言”的 DPDA,我们都能构造出一个与之等价的 DCFG。这个方向的转换是有条件的。
  3. 关键限制条件:带结束标记的语言 (End-marked Language): 这是理解本节内容最重要的前提。
    • 什么是结束标记? 它是一个特殊的、不会出现在原始语言字符串中的符号(比如 $ 或者 \dashv)。这个标记被明确地附加在每个合法字符串的末尾。例如,如果原始语言是 {a, aa, aaa},那么带结束标记的语言就是 {a\dashv, aa\dashv, aaa\dashv}
    • 为什么需要这个限制? 文中解释说,如果没有这个结束标记,DPDADCFG 就不是等价的。
    • 对于 DPDA 来说,加不加结束标记,其识别语言的能力范围(即 确定性上下文无关语言 DCFL没有变化DPDA 只要在读完输入后进入接受状态即可,它自身有机制判断输入是否结束。
    • 但对于 DCFG 来说,结束标记至关重要。没有结束标记的 DCFG 只能生成一类特殊的 DCFL,这类语言被称为“无前缀的”。
  4. 无前缀性质 (Prefix-free):
    • 定义: 一个语言是无前缀的,意味着该语言中没有任何一个字符串是另一个字符串的前缀。例如,集合 {a, ab, b}不是无前缀的,因为 'a''ab' 的前缀。而集合 {a, ba, bb} 就是无前缀的。
    • 结束标记与无前缀的关系: 任何一个带结束标记的语言天然就是无前缀的。因为结束标记 \dashv 不会出现在任何字符串的中间,所以形如 w\dashv 的字符串不可能是另一个字符串 w'\dashv 的前缀(除非 w=w')。例如,'a\dashv' 不可能是 'ab\dashv' 的前缀,因为前者的结尾是 \dashv,而不是 b
    • DCFG 的限制: 如果一个 DCFG 不使用结束标记,它在生成字符串时,很难“知道”一个字符串是否已经“完整”了。这导致它只能生成那些一旦生成完毕就不会成为其他更长字符串开头的语言,即无前缀语言。
💡 [数值示例]
  • 示例1:无前缀 vs 有前缀
  • 语言 $L_1 = \{01, 011, 1\}$。这个语言不是无前缀的,因为字符串 '01' 是字符串 '011' 的前缀。一个 DCFG 很难生成这个语言,因为它在生成 '01' 后会面临“困惑”:是应该结束,还是继续生成 '1' 得到 '011'?确定性文法不允许这种模棱两可。
  • 语言 $L_2 = \{01, 10, 11\}$。这个语言是无前缀的,其中任何一个字符串都不是另一个的前缀。
  • 语言 $L_1$ 加上结束标记 $`: $L_{1, \$} = \{01\$, 011\$, 1\$\}$。这个新语言是**无前缀**的。`'01\$' 不再是 '011\$' 的前缀。
  • 示例2:为什么限制在“带结束标记的语言”
  • 考虑语言 $L = \{a^n \mid n \ge 1\} \cup \{a^n b \mid n \ge 1\}$,这是一个有前缀的 DCFL
  • 一个 DPDA 可以轻松识别它:读一系列 a,如果输入结束,接受;如果读到 b,再读一个 b,然后如果输入结束,接受。这个 DPDA 是确定性的。
  • 但是,很难写出一个 DCFG 来生成它。因为文法在生成一串 a 之后,不知道是该停下来(生成 $a^n$),还是该继续生成一个 b(生成 $a^n b$)。
  • 如果我们使用结束标记 \dashv,语言变成 $L' = \{a^n\dashv \mid n \ge 1\} \cup \{a^n b\dashv \mid n \ge 1\}$。这个语言是无前缀的,现在可以为它设计一个 DCFG(或者更准确地说,一个 LR(k)文法,它与 DCFG 紧密相关)。DPDA 在看到 \dashv 时知道输入结束,而 DCFG 在推导出 \dashv 时知道派生结束。这个明确的结束信号消除了不确定性。
⚠️ [易错点]
  1. 易错点1: 混淆 CFL (上下文无关语言) 和 DCFL (确定性上下文无关语言)。DPDA 识别的是 DCFL,而(非确定性)PDA 识别的是所有 CFLDCFLCFL 的一个真子集。本节讨论的是确定性的情况。
  2. 易错点2: 认为 DCFG 能生成所有的 DCFL。这是错误的。如原文所述,标准的 DCFG 只能生成无前缀DCFL。只有在引入结束标记后,DCFG 的生成能力才扩展到能描述所有 DCFL(以带结束标记的形式)。
  3. 边界情况: 空字符串 ε。如果一个语言包含 ε,它就不可能是无前缀的,因为 ε 是任何非空字符串的前缀。因此,包含 εDCFL 若要被 DCFG 生成,通常也需要借助结束标记,例如将语言表示为 $\{ \dashv, w\dashv, \dots \}$
📝 [总结]

本段的核心思想是建立 DPDADCFG 之间的等价关系。它明确指出,这种等价性并非无条件成立,而是严格限制在“带结束标记的语言”上。这是因为结束标记确保了语言的无前缀性质,解决了 DCFG 在生成字符串时面临的“何时停止”的不确定性问题,从而使其能力与 DPDA 对齐。

🎯 [存在目的]

本段的目的是为后续两个核心引理(引理 2.58引理 2.59)的证明设置背景和约束。在计算机科学理论中,精确地定义模型(如 DPDADCFG)以及它们等价的条件是至关重要的。这确保了理论的严谨性。在实践中,这关系到编译器设计:DPDA 是高效解析算法(如 LR 解析)的理论基础,而 DCFG(或其变体 LR(k) 文法)是描述编程语言语法的工具。证明它们的等价性,意味着我们可以放心地用 DCFG 来设计语言,并相信一定存在一个高效的 DPDA 解析器能够解析它。

🧠 [直觉心智模型]
  1. DPDA: 想象一个只有一个严格指令手册的机器人,它在一条长长的纸带(输入)上从左到右读取字符。它还有一个可以临时存放盘子()的托盘。每读一个字符,它都必须根据指令手册(转移函数)、当前状态和托盘最上面的盘子,做出唯一的、毫不含糊的下一步动作(改变状态、增删盘子)。它在读完纸带后,如果恰好处于某个“开心”状态,就表示接受这条纸带上的字符串。
  2. DCFG: 想象一个乐高大师,他有一套无歧义的拼搭规则(产生式)。例如,“一个‘车’(S) 可以由一个‘车头’(A)加一个‘车尾’(B)组成”。确定性意味着,当你看到一堆零件时,只有一种合法的方式将它们组合成一个更大的部件。
  3. 结束标记: \dashv 就像是火车末尾挂上的守车。无论前面的车厢是什么,只要看到守车,你就知道“这列火车到此结束了”。这为机器人和乐高大师提供了一个明确的停止信号,消除了所有不确定性。
💭 [直观想象]

想象一下用英语写指令。

  1. 有歧义的指令(像非确定性文法):“Go straight and turn when you see a big tree.” (哪里转?左转还是右转?)
  2. 无歧义的指令(像 DCFG):“Go straight for 100 meters, then turn left at the oak tree.” (非常明确)
  3. 现在考虑一个有前缀问题的指令:“Bake the cake.” vs “Bake the cake and add frosting.”。当你完成了 “Bake the cake” 这一步时,你不知道指令是否结束了。
  4. 加上结束标记就像在指令末尾加上一句 “End of instruction.”。现在,“Bake the cake. End of instruction.” 和 “Bake the cake and add frosting. End of instruction.” 就完全没有歧义了。你知道什么时候该停下来。DCFGDPDA 都依赖这种明确的结束信号来保证其确定性行为。

22. 定理 2.57

📜 [原文2]

一个带结束标记的语言可由确定性上下文无关文法生成当且仅当它是确定性上下文无关的。

我们有两个方向需要证明。首先,我们将证明每个 DCFG 都有一个等价的 DPDA。然后,我们将证明每个识别带结束标记的语言DPDA 都有一个等价的 DCFG。我们将在单独的引理中处理这两个方向。

📖 [逐步解释]

这是一个核心定理,它用最简洁的数学语言,正式陈述了上一段引言中描述的核心思想。

  1. 定理陈述分析: "一个带结束标记的语言可由确定性上下文无关文法 (DCFG) 生成当且仅当它是确定性上下文无关 (DCFL) 的。"
    • "一个带结束标记的语言": 这是我们讨论的对象,强调了结束标记 \dashv 的存在。
    • "可由确定性上下文无关文法 (DCFG) 生成": 这是一种描述语言的方式。如果一个语言能被一个 DCFG 的所有产生式推导出来,我们就说它可由该 DCFG 生成。这描述的是语言的 生成 模型。
    • "当且仅当": 这是一个逻辑等价关系,意味着左右两边的论述是完全等价的。它要求我们证明两个方向:
    • 正向 (⇒): 如果一个带结束标记的语言可以由 DCFG 生成,那么它是一个 DCFL
    • 反向 (⇐): 如果一个语言是带结束标记DCFL,那么它一定可以由一个 DCFG 生成。
    • "它是确定性上下文无关的 (DCFL)": 这是另一种描述语言的方式,指的是语言的 识别 模型。一个语言是 DCFL,意味着存在一个确定性下推自动机 (DPDA) 能够识别它。
  2. 定理的本质: 这个定理是在说,对于加了结束标记的语言来说,“能被 DCFG 生成” 和 “能被 DPDA 识别” 这两件事是完全等价的。它正式地在生成模型 (DCFG)识别模型 (DPDA) 之间划上了等号,但前提是语言必须带结束标记
  3. 证明策略: 作者明确指出了证明这个“当且仅当”定理的标准方法——分别证明两个方向。
    • 方向一 (正向证明): 证明 "每个 DCFG 都有一个等价的 DPDA"。这对应了定理中的 "如果...那么..." 部分。这个证明将在引理 2.58 中给出。
    • 方向二 (反向证明): 证明 "每个识别带结束标记语言DPDA 都有一个等价的 DCFG"。这对应了定理中的 "仅当..." 部分。这个证明将在引理 2.59 中给出。
💡 [数值示例]
  • 示例1: 语言 $L = \{a^n b^n \dashv \mid n \ge 0\}$
  • 这个语言是带结束标记的。
  • DCFL 方面: 我们可以轻易构造一个 DPDA 来识别它。读入 a 时,将 a 压栈;读入 b 时,每读一个 b 就弹出一个 a;最后读入 \dashv 时,如果栈为空,则接受。这个过程是完全确定的。所以 $L$ 是一个 DCFL
  • DCFG 方面: 我们可以为它写一个 DCFG: $S \rightarrow aSb \mid \dashv$。这个文法也是确定性的(或者说是 LR(0) 的,即 DCFG)。
  • 定理 2.57 的体现: 这个例子直观地展示了定理的内容。对于语言 $L$,它既是 DCFL(能被 DPDA 识别),也能被 DCFG 生成。定理就是要从理论上证明,这种对应关系对所有带结束标记的 DCFL 都成立。
  • 示例2: 简单的算术表达式语言
  • 考虑一个只包含数字 d 和加号 + 并以 \dashv 结尾的语言,例如 {d\dashv, d+d\dashv, d+d+d\dashv, ...}
  • DCFL 方面: 我们可以构建一个 DPDA 来识别它。它期望一个 d,然后可以跟一个 + 和另一个 d,这个模式可以重复,最后必须是 \dashv。这个检查过程是确定性的。
  • DCFG 方面: 我们可以写一个 DCFG 来生成它:
  • $S \rightarrow E \dashv$
  • $E \rightarrow E + d \mid d$
  • 定理 2.57 的体现: 同样,这个语言既是 DCFL,也可以被 DCFG 生成,符合定理的论述。定理的证明就是要将我们构造 DPDADCFG 的直觉,提升为严格的、普适的算法。
⚠️ [易错点]
  1. 易错点: 再次强调,这个定理的成立依赖于“带结束标记”这个前提。如果去掉这个前提,定理就不成立。例如,有前缀的 DCFL $\{a^n \mid n \ge 1\} \cup \{a^n b \mid n \ge 1\}$ 无法由 DCFG 直接生成。
  2. 边界情况: 定理对包含空串的语言也适用,只要它被表示为带结束标记的形式。例如,如果原始语言是 $\{ \varepsilon, a, b \}$,那么带结束标记的语言是 $\{ \dashv, a\dashv, b\dashv \}$。这个新语言可以由 DPDA 识别,也就可以由 DCFG 生成。
📝 [总结]

定理 2.57 是本节的中心宣言,它精确地指出:在“带结束标记”这个共同的舞台上,确定性上下文无关文法 (DCFG) 的生成能力与确定性下推自动机 (DPDA) 的识别能力是完全等价的。该定理的证明被拆分成两个独立的引理,分别论证从 DCFGDPDA 的转换和从 DPDADCFG 的转换。

🎯 [存在目的]

这个定理的存在是为了在计算理论中建立一个坚实的桥梁。它连接了两种不同但核心的计算模型:一种是描述如何生成合法字符串的语法模型 (DCFG),另一种是描述如何识别合法字符串的机器模型 (DPDA)。这座桥梁的建立,尤其是对确定性模型的关注,直接服务于编程语言的设计与实现。它告诉编译器设计者:你们用来定义语法的工具(DCFG 的变体)和你们用来解析代码的算法(基于 DPDA)在理论上是等效的,这为编译器能够正确、高效地工作提供了理论保障。

🧠 [直觉心智模型]

定理 2.57 就像在说:“对于任何一本有明确‘全书完’标记的书(带结束标记的语言),‘能够按照一套无歧义的语法规则写出这本书’(可由 DCFG 生成)这件事,等价于‘有一个机器人能一字不差地读完这本书并判断它是否符合语法’(是 DCFL)。”

💭 [直观想象]

想象有两个专家:

  1. 建筑师(DCFG): 他有一套非常精确、没有二义性的蓝图(文法规则),用来指导如何从最小的砖块(终结符)一步步搭建成一座宏伟的建筑(字符串)。蓝图的最后一步总是“安装屋顶的避雷针”(结束标记),装上避雷针代表建筑彻底完工。
  2. 质检员(DPDA): 他有一个检测设备和一本严格的检测手册。他从地基开始,一层一层地扫描一座已建成的建筑。他的设备()帮助他记录检测过的结构。每一步检测都是确定性的。当他扫描到屋顶的避雷针时,如果所有检测项都符合手册,他就会给这座建筑盖上“合格”的印章。

定理 2.57 就是在说:只要我们讨论的都是带避雷针的建筑,那么,建筑师能造出来的所有建筑,质检员都能认为是合格的;反之,质检员认为合格的所有建筑,也一定能被某个拥有合格蓝图的建筑师造出来。


2.1 引理 2.58

📜 [原文3]

每个 DCFG 都有一个等价的 DPDA

📖 [逐步解释]

这个引理陈述了定理 2.57 的第一个证明方向:从 DCFGDPDA 的转换是总是可能的。

  1. 引理的核心: 任何一个确定性上下文无关文法 (DCFG),无论它是否生成带结束标记的语言,我们总能找到一个确定性下推自动机 (DPDA),这个 DPDA 识别的语言与该 DCFG 生成的语言完全相同。
  2. “等价”的含义: 这里的“等价”指的是它们描述的语言是同一个集合。即 $L(G) = L(P)$,其中 $G$DCFG$P$ 是等价的 DPDA
  3. 普适性: 注意,这个引理没有“带结束标记”的前提。这意味着从 DCFGDPDA 的转换是普遍成立的。这与反向转换(DPDA → DCFG)形成对比,后者需要结束标记。
📝 [总结]

引理 2.58 声明了一个单向的、无条件的转换关系:总能将任意一个 DCFG 转换成一个功能完全相同的 DPDA。这是证明 DCFGDPDA 等价性的第一步。

🎯 [存在目的]

此引理的存在是为了完成定理 2.57 的正向证明。它建立了一条从生成模型识别模型的构造性路径。在实践中,这意味着如果我们有一个确定性的语法定义(DCFG),我们就有信心能实现一个确定性的解析器(DPDA)来处理它。这构成了 LR 解析等自底向上解析技术的基础。

🧠 [直觉心智模型]

这引理就像在说:“只要你有一本无歧义的乐高拼搭说明书(DCFG),我就能设计一个机器人(DPDA),这个机器人能看到一堆拼好的乐高积木后,判断出它是否是严格按照这本说明书拼出来的。”

💭 [直观想象]

想象一个厨师有一本绝不含糊的菜谱(DCFG),比如“宫保鸡丁:1. 切鸡丁;2. 炒鸡丁;3. 加调料...”。引理 2.58 就是在保证,我们一定可以训练一个美食评论家(DPDA),他品尝一道菜时,能通过一系列明确的步骤(比如先尝鸡丁的火候,再尝酱汁的味道...),准确判断出这道菜是不是严格按照那本“宫保鸡丁”菜谱做出来的。这个过程不需要等到菜的末尾有什么特殊标记,评论家在品尝的每一步都是确定性的。


21.1 证明思路

📜 [原文4]

我们展示如何将 DCFG $G$ 转换为等价的 DPDA $P$$P$ 使用 DFA $DK$ 进行操作,如下所示。它模拟 $DK$ 对从输入中读取的符号,直到 $DK$ 接受。如定理 2.52 的证明所示,$DK$ 的接受状态指示一个特定的带点规则,因为 $G$确定性的,并且该规则识别出扩展到目前为止它已看到的输入的某个有效字符串句柄。此外,由于 $G$确定性的,这个句柄适用于每个有效扩展,特别是如果输入在 $L(G)$ 中,它将适用于 $P$ 的完整输入。因此 $P$ 可以使用这个句柄来识别其输入字符串的第一个规约步骤,即使此时它只读取了部分输入。

$P$ 如何识别第二个和后续的规约步骤?一个想法是直接在输入字符串上执行规约步骤,然后像我们上面那样通过 $DK$ 运行修改后的输入。但是输入既不能被修改也不能被重新读取,所以这个想法行不通。另一种方法是将输入复制到栈上并在那里执行规约步骤,但那样的话 $P$ 需要弹出整个栈才能通过 $DK$ 运行修改后的输入,因此修改后的输入将无法用于后续步骤。

这里的技巧是在栈上存储 $DK$ 的状态,而不是在栈上存储输入字符串。每次 $P$ 读取一个输入符号并模拟 $DK$ 中的一个移动时,它会通过将 $DK$ 的状态压入栈中来记录。当它使用规约规则 $T \rightarrow u$ 执行规约步骤时,它会从栈中弹出 $|u|$ 个状态,显示 $DK$ 在读取 $u$ 之前所处的状态。它将 $DK$ 重置为该状态,然后模拟它在输入 $T$ 上,并将结果状态压入栈中。然后 $P$ 继续读取和处理输入符号,就像以前一样。

$P$开始变量压入栈中时,它已经找到了其输入到开始变量归约,因此它进入接受状态。

📖 [逐步解释]

这部分是构造性证明的核心思想,它描述了一个算法,用于将 DCFG $G$ 转化为 DPDA $P$。这个算法非常巧妙,是 LR 解析的理论雏形。

  1. 核心工具:DFA $DK$
    • 这个证明依赖于一个预先存在的工具,一个特殊的 DFA,名为 $DK$。这个 $DK$ 是从 DCFG $G$ 构造出来的,它的作用是识别 $G$ 的所有有效字符串 (viable prefixes)句柄 (handles)
    • 有效字符串:指的是一个最右推导过程中出现的中间字符串的前缀。
    • 句柄:是最左边的、可以被规约的子串。规约就是将一个产生式的右侧替换回左侧的变量,是推导的逆过程。
    • 因为 $G$DCFG (也叫 LR(0) 文法),它的句柄强制性的,意味着一旦在输入中看到一个句柄,就必须立即进行规约,没有其他选择。$DK$ 正是用来识别这种“必须规约”的信号的。
    • $DK$ 读入一串符号并进入一个接受状态时,就意味着它刚刚读完了一个句柄$DK$ 的接受状态会告诉我们应该用哪个产生式(例如 $T \rightarrow u$)来规约
  2. DPDA $P$ 的工作方式:模拟自底向上的解析
    • 这个 DPDA $P$ 本质上是一个移入-规约 (shift-reduce) 解析器。
    • 移入 (Shift): $P$ 从左到右读取输入字符串。每读一个符号,它就模拟 $DK$ 的行为。这个动作称为“移入”。
    • 规约 (Reduce): 当模拟的 $DK$ 到达一个接受状态时,$P$ 就知道它找到了一个句柄,需要进行规约。例如,如果 $DK$ 指示使用规则 $T \rightarrow u$ 进行规约,那么意味着输入流中最近看到的子串 u 应该被替换为变量 T
  3. 遇到的问题与解决方案
    • 问题: DPDA 的输入带是只读的,不能修改,也不能回头重读。我们怎么在输入带上实现“将 u 替换为 T”这个规约操作呢?
    • 错误想法1: 直接修改输入带。行不通,DPDA 模型不允许。
    • 错误想法2: 把输入复制到栈里,在栈里修改。行不通,因为修改后,你需要从头开始用 $DK$ 重新分析,这就需要把栈清空再读出来,但栈里的内容是后续步骤需要的,不能随便清空。
    • 巧妙的解决方案: 不要在栈里存输入符号,而是在栈里存 $DK$ 的状态!
  4. 最终的算法 (核心技巧)
    • 初始化: $P$ 的栈初始时只有一个元素,即 $DK$ 的开始状态 $q_{start}$
    • 移入 (Shift): $P$ 读取一个输入符号 a。它查看栈顶的 $DK$ 状态 $s_{top}$,然后计算 $DK$ 在状态 $s_{top}$ 读入 a 后会转移到哪个新状态 $s_{new}$。然后,$P$$s_{new}$ 压入栈。这个过程不断重复。
    • 规约 (Reduce): 假设在某一步之后,栈顶的状态是 $s_{accept}$,这是一个 $DK$ 的接受状态。这个状态告诉 $P$ 需要用规则 $T \rightarrow u$ 进行规约,其中 $u$ 的长度是 $|u|$
    • $P$ 从自己的栈中弹出 $|u|$ 个状态。
    • 弹出后,栈顶现在是 $s_{before}$,这个状态就是 $DK$ 在开始读取 u 之前所处的状态。
    • 现在, $P$ 假装已经看到了变量 T。它查询 $DK$ 在状态 $s_{before}$ “读取” T 后会去哪个状态,我们称之为 $s_{after_T}$
    • $P$$s_{after_T}$ 压入栈。
    • 这样就完成了一次规约,栈的内容反映了规约后的状态,而输入指针没有回退。
    • 接受: 当 $P$ 成功地将整个输入规约为文法的开始变量 (例如,通过规则 $S' \rightarrow S$ 进行规约) 时,它就进入接受状态,成功识别了输入字符串。
💡 [数值示例]
  • 示例: 文法 $G$: $E \rightarrow aEa \mid b$,输入字符串 $aba$
  • 这是一个简化的 DCFG。我们假设已经为它构造好了 DFA $DK$
  • DPDA $P$ 解析 $aba$ 的过程: (状态用 $s_i$ 表示)
  1. 初始状态: 栈: [$s_0$],输入: ^aba$
  2. 移入 'a': 读 a。假设 $\delta_{DK}(s_0, a) = s_1$。栈: [$s_0, s_1$],输入: a^ba$
  3. 移入 'b': 读 b。假设 $\delta_{DK}(s_1, b) = s_2$。栈: [$s_0, s_1, s_2$],输入: ab^a$
  4. 规约 bE: 此时,$s_2$$DK$ 的一个接受状态,对应规则 $E \rightarrow b$。句柄是 b,长度为 1。
    • 从栈中弹出 1 个状态 ($s_2$)。栈顶现在是 $s_1$
    • $P$ 需要知道 $DK$$s_1$ 状态读入 E 会去哪里。假设 $\delta_{DK}(s_1, E) = s_3$
    • $s_3$ 压栈。
    • 规约后状态: 栈: [$s_0, s_1, s_3$],输入: ab^a$ (输入指针不变)
  5. 移入 'a': 读 a。假设 $\delta_{DK}(s_3, a) = s_4$。栈: [$s_0, s_1, s_3, s_4$],输入: aba^$
  6. 规约 aEaE: 此时,$s_4$$DK$ 的接受状态,对应规则 $E \rightarrow aEa$。句柄是 aEa,长度为 3。注意,这里的 E 是上一步规约的结果,它体现在栈中的状态序列上。
    • 从栈中弹出 3 个状态 ($s_4, s_3, s_1$)。栈顶现在是 $s_0$
    • 查询 $DK$$s_0$ 状态读入 E 会去哪里。假设 $\delta_{DK}(s_0, E) = s_5$
    • $s_5$ 压栈。
    • 规约后状态: 栈: [$s_0, s_5$],输入: aba^$
  7. 接受: 此时,我们已经将整个输入 aba 规约成了 E(文法的开始变量)。如果 $s_5$ 是一个最终的接受状态(对应于规约到开始变量),则 $P$ 接受字符串 aba
⚠️ [易错点]
  1. 易错点: 最核心的易错点就是理解为什么栈里存的是状态而不是符号。存符号无法解决输入带不能回退的问题。存状态,则巧妙地将“已经解析过的历史”编码在栈里。每次规约,通过弹出状态,实际上是在“撤销”最近的移入步骤,回到规约发生前的解析状态,然后“应用”规约结果(即压入一个新的状态),从而在不接触输入带的情况下前进。
  2. 边界情况: 空字符串 ε 的处理。如果文法可以生成 ε (例如 $T \rightarrow \varepsilon$),那么在某个状态下,不需要读取任何输入,就可以进行一次长度为 0 的规约。DPDA 会在不消耗输入的情况下,弹出 0 个状态,然后根据当前栈顶状态和规约后的变量 T 压入新状态。
📝 [总结]

该证明思路描述了一个精巧的算法,用于将任何 DCFG $G$ 转换为等效的 DPDA $P$。其核心技巧是让 DPDA $P$ 的栈存储一个辅助性的 DFA ($DK$) 的状态序列,而不是输入符号本身。$P$ 通过“移入”操作(读取输入,压入新状态)和“规约”操作(根据 $DK$ 的信号,弹出若干状态,再压入一个代表规约结果的新状态),来模拟一个自底向上的解析过程。这个过程是确定性的,因为它完全由 DCFG 的确定性(体现在 $DK$ 上)来指导。

🎯 [存在目的]

这个证明思路的存在,不仅是为了完成引理 2.58 的证明,更重要的是,它揭示了确定性解析的底层工作原理。它为现代编译器中广泛使用的 LR(0)SLRLALRLR(1) 等解析算法提供了理论模型。它告诉我们,只要语法是确定性的,我们就可以构建一个状态机(DPDA),用一个栈来高效地、确定地解析它,而无需回溯。

🧠 [直觉心智模型]

想象你在玩一个复杂的解谜游戏,面前有一长串按顺序排列的线索(输入字符串)。

  1. 你的大脑是 DPDA,你的记事本是栈
  2. 线索手册 ($DK$) 告诉你看到某些线索组合(句柄)时,它们可以合并成一个“高级线索”(变量)。
  3. 你不能回头重读线索,只能一直往前走。
  4. 移入: 每看到一个新线索,你不会在记事本上写下线索本身,而是写下“我看到了这个线索后,我的理解状态是什么”。
  5. 规约: 突然,你发现记事本上最近几条“理解状态”的序列,正好对应了线索手册里的一条合并规则(比如“状态A-状态B-状态C”序列意味着你看到了“匕首-血迹-脚印”的组合,可以合并为“谋杀案发生”这个高级线索)。
  6. 你擦掉记事本上最后那几条状态,回忆起在进入状态A之前你的理解状态是什么(看记事本上剩下的最后一条),然后直接写下“我理解了‘谋杀案发生’后,我的新理解状态是什么”。
  7. 整个过程,你只读了一遍线索,但通过在记事本上操作你的理解状态,你成功地把一长串低级线索,规约成了“谜底揭晓”这个最终的高级线索。
💭 [直观想象]

把解析过程想象成坐火车。

  1. 输入是前方的铁轨和沿途的风景。
  2. DPDA 是火车司机。
  3. 是火车已经挂上的车厢序列。
  4. $DK$ 的状态是每节车厢的“类型牌”(例如,“乘客车厢”、“餐车”、“卧铺车厢”)。
  5. 移入: 火车前进一公里,看到新的风景(输入符号),司机根据当前最后一节车厢的类型和新风景,决定挂上一节什么样的新车厢。他把新车厢的“类型牌”挂上。
  6. 规约: 司机发现最后三节车厢的类型牌序列是“卧铺-卧铺-餐车”,调度手册($DK$)说这是一个标准的“豪华旅行包厢”组合(句柄),可以被规约
  7. 司机并不需要把火车倒回去。他只是走到倒数第四节车厢那里,把最后三节车厢(连同它们的类型牌)都拆掉,然后根据倒数第四节车厢的类型和“豪华旅行包厢”这个概念,挂上一节全新的、挂着“豪华旅行包厢”类型牌的车厢。
  8. 火车(栈)变短了,然后又变长了一点,但总的来说,它在概念上被简化了。火车头(输入指针)始终在向前开。最终,如果整列火车被成功组合成一节标有“S-Class 特快列车”(开始变量)的车厢,那么这次旅途(解析)就成功了。

32.2 引理 2.59

📜 [原文5]

每个识别带结束标记语言DPDA 都有一个等价的 DCFG

📖 [逐步解释]

这个引理是定理 2.57 的第二个、也是更复杂的一个证明方向。它要说明如何从一个识别带结束标记语言DPDA 出发,反向构造出一个等价的 DCFG

  1. 引理的核心: 对于任何一个确定性下推自动机 (DPDA),只要它识别的语言是带结束标记的(即所有合法字符串都以特殊符号 \dashv 结尾),那么我们一定能构造出一个确定性上下文无关文法 (DCFG),这个 DCFG 生成的语言与该 DPDA 识别的语言完全相同。
  2. 关键前提: “带结束标记语言”。这个前提在这里至关重要,证明过程会严重依赖这个特性。没有这个前提,引理不成立。一个 DPDA 可以识别有前缀的语言,但一个 DCFG 无法生成它们,所以必须通过结束标记来消除前缀,使得两者能力对齐。
  3. 等价性: 同样,$L(P) = L(G)$,其中 $P$ 是给定的 DPDA$G$ 是我们构造出的 DCFG
📝 [总结]

引理 2.59 声明了一个单向的、带条件的转换关系:总能将任意一个识别带结束标记语言DPDA,转换成一个功能完全相同的 DCFG。这是证明 DPDADCFG 等价性的第二步,也是更难的一步。

🎯 [存在目的]

此引理的存在是为了完成定理 2.57 的反向证明。它建立了一条从识别模型回到生成模型的构造性路径。这在理论上完成了等价性的闭环。它表明,任何能被确定性地、机械地解析的(带结束标记的)语言结构,也一定能用一套确定性的、无歧义的语法规则来描述。这再次加强了语法设计和解析器实现之间的理论联系。

🧠 [直觉心智模型]

此引理就像在说:“只要你有一个机器人(DPDA),它能准确地判断任何一本以‘全书完’(结束标记)结尾的书是否符合某种未知的语法,我就能通过观察这个机器人的所有行为,反向推导出一本无歧义的语法书(DCFG),这本语法书恰好能写出所有被机器人判定为‘好书’的书。”

💭 [直观想象]

想象你有一个非常挑剔的锁匠(DPDA),他只认一种带特殊标记(比如末端有个小铃铛 \dashv)的钥匙。他能通过一套固定的、确定性的步骤(转动、插入、感知内部结构),判断一把带铃铛的钥匙是不是“对的钥匙”。

引理 2.59 就是在保证,我们可以通过彻底研究这个锁匠检测钥匙的所有动作(状态如何变化、何时利用了钥匙的哪个凹槽),反向工程出一套制造这种“对的钥匙”的精确图纸(DCFG)。这套图纸会告诉我们,哪里该有个凹槽,哪里该有个凸起,最后必须装上那个小铃铛。用这套图纸造出来的所有钥匙,都能被那个挑剔的锁匠打开锁。铃铛(结束标记)之所以重要,是因为它给了锁匠一个明确的信号:“钥匙插到底了,现在做最终判断”,这个信号简化了我们反向推导图纸的难度。


22.1 证明思路

📜 [原文6]

该证明是引理 2.27 第 121 页构造的修改,该引理描述了 PDA $P$ 到等价 CFG $G$ 的转换。

这里 $P$$G$确定性的。在引理 2.27 的证明思路中,我们修改了 $P$,使其在接受时清空栈并进入一个特定的接受状态 $q_{\text{accept}}$PDA 无法直接确定它是否在输入的末尾,因此 $P$ 利用其非确定性来猜测它处于这种情况。我们不想在构造 DPDA $P$ 时引入非确定性。相反,我们使用 $L(P)$带结束标记的假设。我们修改 $P$,使其在读取结束标记 $\dashv$ 后进入其原始接受状态之一时,清空栈并进入 $q_{\text{accept}}$

接下来,我们应用文法构造来获得 $G$。简单地将原始构造应用于 DPDA 会产生一个几乎确定性文法,因为 CFG推导DPDA 的计算密切对应。该文法在一个微小但可修复的方面未能实现确定性

原始构造引入了形式为 $A_{pq} \rightarrow A_{pr} A_{rq}$ 的规则,这些规则可能导致歧义。这些规则涵盖了 $A_{pq}$ 生成一个字符串的情况,该字符串将 $P$ 从状态 $p$ 带到状态 $q$,其栈在两端都为空,并且栈中途清空。替换对应于在该点划分计算。但是,如果栈多次清空,则可能进行多次划分。每个划分都会产生不同的分析树,因此所得文法有歧义的。我们通过修改文法来解决这个问题,使其仅在栈中途清空的最后一点划分计算,从而消除这种歧义。例如,在有歧义文法中也出现了类似但更简单的情况:

$$ \begin{aligned} & S \rightarrow T \dashv \\ & T \rightarrow T T|(T)| \varepsilon \end{aligned} $$

这等价于无歧义确定性文法

$$ \begin{aligned} & S \rightarrow T \dashv \\ & T \rightarrow T(T) \mid \varepsilon . \end{aligned} $$

接下来我们使用 DK 检验来证明 $G$确定性的。为此,我们将分析 $P$ 如何在有效字符串上操作,通过扩展其输入字母表和转移函数来处理变量符号终结符号。我们将所有符号 $A_{pq}$ 添加到 $P$ 的输入字母表,并通过定义 $\delta\left(p, A_{p q}, \boldsymbol{\varepsilon}\right)=(q, \boldsymbol{\varepsilon})$ 来扩展其转移函数 $\delta$。将所有其他涉及 $A_{pq}$ 的转移设置为 $\emptyset$。为了保持 $P$确定性行为,如果 $P$ 从输入中读取 $A_{pq}$,则不允许 $\varepsilon$ 输入移动。

📖 [逐步解释]

这部分概述了将 DPDA 转换为 DCFG 的证明策略。它不是一个全新的发明,而是对一个已知的通用构造(从任意 PDACFG)的精巧改造,使其适应确定性的约束。

  1. 基础:PDA 到 CFG 的通用构造 (引理 2.27)
    • 证明的基础是一个已知的定理:任何一个(不一定是确定性的)PDA 都能被转换成一个等价的 CFG
    • 这个通用构造的核心思想是创建形如 $A_{pq}$文法变量,其中 $A_{pq}$ 的目标是生成所有能让 PDA 从状态 $p$ 开始,到状态 $q$ 结束,并且最终栈状态与初始栈状态相同的字符串。 essentially "net stack change is zero".
    • 例如,如果 PDA 从状态 $p$ 读入字符 a 并将 X 压栈,然后转移到状态 $p_1$,之后从 $p_1$ 开始经过一系列计算消耗了字符串 w 后,最终回到状态 $p_2$ 并恰好将 X 弹出,那么文法中就会有一条规则,将这个过程编码,例如 $A_{pp_2} \rightarrow a A_{p_1 p_2} \dots$
  2. 为确定性进行改造 (第一步): 处理接受状态
    • 在通用的 PDA->CFG 构造中,为了简化,通常会修改 PDA,使其满足“接受前清空栈”的特性。
    • 对于一个非确定性 PDA,它可以通过“猜测”自己已经读到输入末尾,然后用一系列 ε 转移来清空栈,最后进入唯一的接受状态 $q_{\text{accept}}$
    • 但我们的 DPDA 不能“猜测”,它的每一步都是确定的。这里,结束标记 \dashv 派上了用场。
    • 改造方法: 我们修改给定的 DPDA $P$。新的 $P'$ 的行为和 $P$ 完全一样,直到它读到结束标记 \dashv。当 $P'$ 读到 \dashv 并且进入了 $P$ 的一个原始接受状态时,它就知道输入真的结束了。此时,$P'$ 才开始清空它的栈,并最终转移到一个全新的、唯一的接受状态 $q_{\text{accept}}$。因为 \dashv 只在末尾出现一次,这个过程仍然是确定性的。
  3. 应用构造并修复歧义 (第二步): 构造文法并消除不确定性
    • 现在我们对改造后的 DPDA $P'$ 使用标准的 PDA->CFG 构造算法,得到一个文法 $G$
    • 由于 $P'$ 是确定性的,它生成的文法 $G$ “几乎”是确定性的 (DCFG)。但有一个小问题会导致歧义
    • 歧义的来源: 来源于形如 $A_{pq} \rightarrow A_{pr} A_{rq}$ 的规则。这种规则的含义是:要从状态 $p$ 到状态 $q$ 且净栈变化为零,可以先从 $p$$r$(净栈变化为零),再从 $r$$q$(净栈变化为零)。如果从 $p$$q$ 的过程中,栈多次变为空(回到了起始高度),那么就可以在任何一个栈变为空的中间点 $r$ 进行切分,导致一个字符串有多种推导方式(即多种分析树),这就是歧义
    • 消除歧义的方法: 修改文法构造规则,强制只在最后一次栈变为空的地方进行切分。这样,对于任何一个计算过程,切分点都是唯一的,从而消除了这类歧义。
    • 文中的例子:
    • 有歧义文法: $T \rightarrow T T | (T) | \varepsilon$。字符串 ()() 可以有两种推导:$(T \rightarrow TT) \rightarrow (T \rightarrow (T))T \rightarrow ()T \rightarrow ()(T) \rightarrow ()()$ 或者 $(T \rightarrow TT) \rightarrow T(T \rightarrow (T)) \rightarrow T() \rightarrow (T)T() \rightarrow ()()$。这两种推导对应不同的分析树。
    • 无歧义文法: $T \rightarrow T(T) \mid \varepsilon$。这个文法强制要求 () 成对出现,并且是右递归的,避免了上述的歧义。这只是一个类比,说明通过修改规则可以消除歧义。实际的修改是在 $A_{pq} \rightarrow \dots$ 规则上。
  4. 最终验证 (第三步): 使用 DK 检验
    • 在获得了修正后的文法 $G$ 之后,我们需要严格地证明它确实是一个 DCFG
    • 我们使用 DK 检验(在前面证明引理 2.58 时提到过的工具)来证明 $G$ 的确定性。
    • 为了进行检验,我们需要分析 DPDA $P$ 在遇到文法 $G$变量(如 $A_{pq}$)时会如何表现。
    • 技巧: 假想地扩展 DPDA $P$ 的能力,让它不仅能读输入符号(如 a, b),也能直接“读取”我们构造出的文法变量 $A_{pq}$。我们定义,当 $P$ 在状态 $p$ 时,如果它“读取”了 $A_{pq}$,它就不消耗任何实际输入,也不动栈,直接跳转到状态 $q$。即 $\delta(p, A_{pq}, \varepsilon) = (q, \varepsilon)$
    • 有了这个扩展的 $P$,我们就可以分析 $G$ 的推导过程,并最终证明 $G$ 满足 DK 检验的所有条件,从而证明 $G$ 是一个 DCFG
∑ [公式拆解]
  • 公式1: 有歧义文法

$$ \begin{aligned} & S \rightarrow T \dashv \\ & T \rightarrow T T|(T)| \varepsilon \end{aligned} $$

  • $S$: 开始变量。
  • $T$: 一个变量,代表某种平衡的结构。
  • $\dashv$: 结束标记。
  • $T \rightarrow T T$: 这是歧义的根源。它表示一个 $T$ 结构可以由两个相邻的 $T$ 结构组成。这就像说“一个句子可以由两个句子组成”,但没说怎么切分。
  • $T \rightarrow (T)$: 表示一个 $T$ 结构可以被括号包围。
  • $T \rightarrow \varepsilon$: 表示 $T$ 可以是空串。
  • 这个文法可以生成像 ()() 这样的字符串,但推导路径不唯一。
  • 公式2: 无歧义文法

$$ \begin{aligned} & S \rightarrow T \dashv \\ & T \rightarrow T(T) \mid \varepsilon . \end{aligned} $$

  • $T \rightarrow T(T)$: 这是一个右递归的规则。它表示一个 $T$ 结构,后面可以跟一个被括号包围的 $T$ 结构。要生成 ()(),唯一的推导方式是 $S \Rightarrow T\dashv \Rightarrow T(T)\dashv \Rightarrow T()\dashv \Rightarrow (T)T()\dashv \Rightarrow ...$ (这里为了展示右递归,我稍微改了下规则,原文的例子 $T \rightarrow T(T)$ 已经体现了思想)。原文的例子 $T \rightarrow T(T) \mid \varepsilon$ 生成 ()() 的推导是:$T \Rightarrow T(T) \Rightarrow T() \Rightarrow T(T)(T) \Rightarrow \varepsilon (T) () \Rightarrow (T) () \Rightarrow (\varepsilon) () \Rightarrow ()()$。等等,这个例子似乎有点问题。让我们理解作者的意图:通过改变规则结构,强制唯一的推导顺序。一个更经典的无歧义版本是 $T \rightarrow (T)T \mid \varepsilon$。用这个规则推导 ()(): $T \Rightarrow (T)T \Rightarrow ()T \Rightarrow ()(T)T \Rightarrow ()()T \Rightarrow ()() \varepsilon = ()()$。推导是唯一的。作者的例子 $T \rightarrow T(T) \mid \varepsilon$ 可能是想表达类似的思想。
  • 公式3: 扩展转移函数

$$ \delta\left(p, A_{p q}, \boldsymbol{\varepsilon}\right)=(q, \boldsymbol{\varepsilon}) $$

  • $\delta$: DPDA $P$转移函数
  • $p$: 当前状态。
  • $A_{pq}$: 我们假想的一个输入符号,它代表文法中的一个变量
  • $\varepsilon$ (输入位置): 表示这个转移不消耗任何实际的输入带上的符号。
  • $\varepsilon$ (栈操作位置): 表示这个转移对栈没有影响(既不压入也不弹出)。
  • $(q, \varepsilon)$: 转移的结果是进入新状态 $q$,并且不对栈进行操作。
  • 含义: 这条规则是在我们的思想实验中,赋予 DPDA 理解文法变量的能力。如果 DPDA 在状态 $p$ “看到”了变量 $A_{pq}$,它就直接“跃迁”到状态 $q$。这完美地对应了变量 $A_{pq}$ 的定义:“生成一个能让 DPDA$p$ 走到 $q$ 的字符串”。
💡 [数值示例]
  • 示例: 歧义规则 $A_{pq} \rightarrow A_{pr} A_{rq}$ 的影响
  • 假设一个 DPDA 有状态 $p, r, q$
  • 假设字符串 $w_1$ 能让 DPDA$p$ 走到 $r$(栈净变化为零)。
  • 假设字符串 $w_2$ 能让 DPDA$r$ 走到 $q$(栈净变化为零)。
  • 那么字符串 $w = w_1 w_2$ 就能让 DPDA$p$ 走到 $q$(栈净变化为零)。
  • 文法中就会有规则 $A_{pr} \rightarrow w_1$$A_{rq} \rightarrow w_2$$A_{pq} \rightarrow w_1 w_2$
  • 同时,也有一条“通用”规则 $A_{pq} \rightarrow A_{pr} A_{rq}$
  • 现在,推导 $w$ 就可以有两种方式:
  1. $A_{pq} \Rightarrow w_1 w_2$ (直接用长规则)
  2. $A_{pq} \Rightarrow A_{pr} A_{rq} \Rightarrow w_1 A_{rq} \Rightarrow w_1 w_2$ (通过通用规则)
    • 这造成了歧义
    • 修复: 构造文法时,规定 $A_{pq} \rightarrow A_{pr} A_{rq}$ 这类规则只在 $r$$p \rightarrow q$ 路径上最后一个栈回到初始高度的点时才生成。这样,切分点 $r$ 就唯一了,消除了歧义。
⚠️ [易错点]
  1. 易错点: 认为 PDA->CFG 的标准构造可以直接产生 DCFG。这是错误的,必须经过两个关键修改:1) 利用结束标记来确定性地清空栈;2) 修改文法规则的生成方式以消除由“中途栈空”引起的歧义。
  2. 边界情况: 如果 DPDA 在接受过程中,栈从未回到过初始高度(除了开始和结束),那么就不会产生 $A_{pq} \rightarrow A_{pr} A_{rq}$ 形式的规则,也就没有这种类型的歧义。但证明必须考虑最一般的情况。
📝 [总结]

该证明思路的核心是“改造并应用”。它首先改造给定的 DPDA,利用结束标记 \dashv 来实现确定性的“接受前清空栈”。然后,它应用一个已知的、通用的 PDA->CFG 构造算法。但它敏锐地发现这个算法会引入一类特定的歧义,并通过修改规则生成方式(强制在最后一次栈空点切分)来修复这个缺陷。最后,它计划使用 DK 检验来严格证明最终得到的文法 $G$ 确实是确定性的(DCFG)。

🎯 [存在目的]

本段的目的是勾勒出一条从一个复杂的机器模型(DPDA)回归到一个同样有严格约束的语法模型(DCFG)的构造性路径。这条路径充满了陷阱(非确定性、歧义),本段清晰地指出了这些陷阱,并提出了解决方案。这展示了理论证明的精妙之处:不是简单地应用公式,而是在理解其背后原理的基础上,进行巧妙的修改和调整,以适应更严格的约束条件。

🧠 [直觉心智模型]

这个过程就像破解一个黑盒程序

  1. 黑盒程序: 就是给定的 DPDA。你知道它的输入和输出(接受或拒绝),还能观测它内部的一些状态变化。
  2. 目标: 写出这个程序的“设计文档”(DCFG)。
  3. 第一步: 你发现这个程序在结束时行为有点复杂,你就给它加一个“钩子”(结束标记),让它在退出前必须执行一段你指定的、干净的清理代码(清空栈)。
  4. 第二步: 你使用一个通用的“反编译器”(PDA->CFG 构造)去分析这个加了钩子的程序。
  5. 第三步: 反编译器生成的代码(文法)能用,但有些地方看起来很啰嗦,甚至有多种解释方式(歧义)。你发现这是因为反编译器在处理一个循环或复合逻辑时,会在每个可能的中间点都生成一种解释。
  6. 第四步: 你给反编译器打了个补丁,告诉它:“在分析一段复合逻辑时,只在最后一个逻辑断点处生成注释和结构。”(修复歧义)。
  7. 最后: 你拿着打过补丁的反编译器生成的“设计文档”,用一套行业标准(DK 检验)来审查,最终证明这份文档是清晰、无歧义的(是 DCFG)。
💭 [直观想象]

想象你在考古,发现了一台古代的自动织布机(DPDA)。这台机器能检验一块织好的布匹(带结束流苏 \dashv 的字符串)是否是“皇家贡品”。

  1. PDA->CFG 构造: 就像一个通用的方法,通过观察织布机的所有齿轮和杠杆运动,来反推织布的图纸(CFG)。
  2. 改造 DPDA: 你发现这台机器停机时很随意。于是你改装了它,让它在检测到布匹末端的流苏时,必须把所有梭子和线头都归位到一个标准初始状态(清空栈),才算检测完毕。
  3. 歧义问题: 你发现反推出来的图纸上,有一段复杂的花纹,图纸上画着“先织左半边再织右半边”和“先织中间再织两边”两种方法,都能织出同样的花纹。这在图纸上是歧义的。
  4. 修复歧义: 你仔细研究机器的动作,发现它实际上总是先把其他部分都织完,最后才织某个连接部分。于是你在反推图纸的规则里加了一条:“当遇到可以拆分的复杂花纹时,必须以机器最后处理的那个连接点作为拆分依据来画图。”
  5. 最终证明: 有了这张修正后的、无歧义的图纸(DCFG),你用一套名为“皇家纺织工艺标准”(DK 检验)的文献来核对,最终证明这张图纸是完全符合标准的、确定性的皇家图纸。

22.2 断言 2.60

📜 [原文7]

以下断言适用于 $L(G)$ 中任何字符串 $w$推导,例如

$$ A_{q_{0}, q_{\text {accept}}}=v_{0} \Rightarrow v_{1} \Rightarrow \cdots \Rightarrow v_{i} \Rightarrow \cdots \Rightarrow v_{k}=w $$

如果 $P$ 读取包含变量 $A_{pq}$$v_i$,它会在读取 $A_{pq}$ 之前进入状态 $p$

📖 [逐步解释]

这个断言是在证明“我们构造出的文法 $G$ 是 DCFG”这个大目标过程中的一个关键辅助结论。它试图在文法 G 的推导过程DPDA P 的计算过程之间建立一个精确的对应关系。

  1. 断言的对象:
    • $L(G)$: 我们从 DPDA $P$ 构造出来的文法 $G$ 所生成的语言。
    • $w$: $L(G)$ 中的任意一个字符串。
    • $A_{q_{0}, q_{\text {accept}}}=v_{0} \Rightarrow v_{1} \Rightarrow \cdots \Rightarrow v_{k}=w$: 这是 $w$ 在文法 $G$ 中的一个最右推导序列。
    • $A_{q_{0}, q_{\text {accept}}}$: 这是文法 $G$ 的开始变量,它的设计目标就是生成所有能让 DPDA $P$ 从开始状态 $q_0$ 走到最终接受状态 $q_{\text{accept}}$ 的字符串。
    • $v_i$: 推导过程中的第 $i$ 个中间字符串,它可能包含终结符(如 a, b)和变量(如 $A_{rs}$)。
    • $\Rightarrow$: 表示一步推导。
  2. 断言的核心内容:
    • "如果 $P$ 读取包含变量 $A_{pq}$$v_i$" : 这里的“读取”是一个思想实验。我们假想 DPDA $P$ 不仅能读 a, b 等普通符号,也能“读取”像 $A_{pq}$ 这样的文法变量。正如之前定义的,当 $P$ 在状态 $p$ “读取” $A_{pq}$ 时,它会直接跳转到状态 $q$
    • "...它会在读取 $A_{pq}$ 之前进入状态 $p$。" : 这是断言的关键。它声称,在 DPDA $P$ 模拟运行到中间推导形式 $v_i$$A_{pq}$ 这个位置时, $P$ 的内部状态正好$p$
  3. 断言的意义: 这个断言将文法变量的下标 ($p$) 与 DPDA运行时状态精确地联系了起来。变量 $A_{pq}$ 被设计出来的初衷就是为了表示“从状态 $p$ 到状态 $q$ 的一段计算”,这个断言证明了在任何推导步骤中,这个设计意图都得到了忠实的保持。这就像是在说,我们给变量起的名字 ($A_{pq}$) 不是随便起的,它在每时每刻都准确地反映了 DPDA 的行为。
∑ [公式拆解]

$$ A_{q_{0}, q_{\text {accept}}}=v_{0} \Rightarrow v_{1} \Rightarrow \cdots \Rightarrow v_{i} \Rightarrow \cdots \Rightarrow v_{k}=w $$

  • $A_{q_{0}, q_{\text {accept}}}$: 文法的开始变量。下标 $q_0$DPDA $P$ 的初始状态,而 $q_{\text{accept}}$ 是我们为 $P$ 设计的那个唯一的、清空栈后进入的最终接受状态。这个变量的含义是“生成一个能让 $P$$q_0$ 走到 $q_{\text{accept}}$ 的完整字符串”。
  • $v_0, v_1, \dots, v_k$: 一个推导序列 (derivation sequence)$v_0$ 是开始变量本身。$v_{i+1}$ 是由 $v_i$ 应用了 $G$ 中的一条产生式规则得到的。
  • $v_i$: 称为一个句型 (sentential form)。如果它只包含终结符,它就是一个语言中的字符串。如果它包含变量,它就是一个中间句型。
  • $w$: 推导的最终结果,是语言 $L(G)$ 中的一个字符串,它只包含终结符。
  • $\Rightarrow$: 一步推导的符号。$v_i \Rightarrow v_{i+1}$ 意味着在 $v_i$ 中找到了一个变量,并将其替换为该变量对应的一条产生式的右侧,从而得到 $v_{i+1}$
💡 [数值示例]
  • 假设 DPDA $P$ 有状态 $\{q_0, q_1, q_2, q_3\}$
  • 假设文法 $G$ 有一个推导步骤:$v_i = a A_{q_1 q_2} b \Rightarrow a c A_{q_3 q_2} d b = v_{i+1}$
  • 这一步意味着在 $v_i$ 中,变量 $A_{q_1 q_2}$ 被规则 $A_{q_1 q_2} \rightarrow c A_{q_3 q_2} d$ 替换了。
  • 断言 2.60 对 $v_i$ 的应用: 当 DPDA $P$ "读取" $v_i$ 时:
  1. $P$ 从初始状态 $q_0$ 开始,读取 a,假设它进入了状态 $q_1$
  2. 此时,DPDA 将要“读取”变量 $A_{q_1 q_2}$。断言声称,此刻 DPDA 的状态必须$q_1$(变量的第一个下标)。这与我们的假设相符。
    • 断言 2.60 对 $v_{i+1}$ 的应用: 当 DPDA $P$ "读取" $v_{i+1}$ 时:
  3. $P$$q_0$a,进入 $q_1$
  4. 接着读 c,假设从 $q_1$c 会进入状态 $q_3$
  5. 此时,DPDA 将要“读取”变量 $A_{q_3 q_2}$。断言声称,此刻 DPDA 的状态必须$q_3$(变量的第一个下标)。这也与我们的计算相符。
    • 这个例子直观地显示了断言的合理性:推导规则的构造和 DPDA 的状态转移是同步的、一致的。
⚠️ [易错点]
  1. 易错点: 将此处的“读取”与 DPDA 真实地消耗输入混淆。这里的“读取”$A_{pq}$ 是一个定义在证明过程中的抽象操作,它不消耗输入带上的符号,而是直接改变 DPDA 的状态。
  2. 边界情况: 基础情况 $i=0$。此时 $v_0 = A_{q_0, q_{\text{accept}}}$DPDA 还没开始读取任何东西,处于初始状态 $q_0$。此时它将要“读取”的变量是 $A_{q_0, q_{\text{accept}}}$,变量的第一个下标是 $q_0$。断言成立。
📝 [总结]

断言 2.60 建立了一座坚实的桥梁,它声明在文法 $G$ 的任何一步推导中间形式 $v_i$ 中,如果存在一个变量 $A_{pq}$,那么当模拟的 DPDA $P$ 的计算恰好进行到这个变量的位置时,$P$ 的内部状态不多不少正好就是 $p$。这个断言保证了我们构造的文法变量的下标与其在 DPDA 计算中所对应的状态是完美同步的。

🎯 [存在目的]

这个断言是后续使用 DK 检验来证明文法 $G$ 的确定性的基石。DK 检验本身是分析文法结构的,但它的原理源于模拟解析过程。为了能将 DK 检验应用于我们从 DPDA 构造出的文法 $G$,我们必须首先证明 $G$ 的推导过程和 $P$ 的计算过程之间存在着这种可预测的、严格的对应关系。断言 2.60 就是这个对应关系的核心。有了它,我们才能在分析文法时,有把握地说“这里有一个变量 $A_{pq}$,所以对应的 DPDA 状态一定是 $p$”,从而利用 DPDA 的确定性来推导文法的确定性。

🧠 [直觉心智模型]

这就像给地图上的每个城市都用经纬度作了标记。

  1. DPDA 的状态: 就像是你的 GPS 实时位置。
  2. 文法变量 $A_{pq}$: 就像地图上一条从城市 P 到城市 Q 的高速公路的标记。
  3. 断言 2.60: 就像在说:“无论你沿着哪条路线(推导序列)旅行,当你开车(DPDA 计算)到达地图上标记为‘G101 高速公路’(变量 $A_{pq}$)的入口时,你的 GPS 位置(DPDA 状态)必然显示你就在‘城市 P’(状态 $p$)。”这个断言保证了地图标记和你的实际位置是永远同步的。
💭 [直观想象]

想象一个大型接力赛。

  1. DPDA 的状态 $\{p, q, r, ...\}$: 代表接力赛中的不同交接棒区域。
  2. 文法变量 $A_{pq}$: 代表一张“任务卡”,上面写着“请一名或多名选手,从 $p$ 区跑到 $q$ 区”。
  3. 推导 $A_{pq} \Rightarrow a A_{rs} b$: 意味着完成“从 $p$$q$”这个大任务,可以分解为:选手先跑一段路 a 到达 $r$ 区,然后交给下一名选手完成“从 $r$$s$”的任务($A_{rs}$),该选手完成后再跑一段路 b,最终到达 $q$ 区。
  4. 断言 2.60: 保证了这个任务分解的逻辑和实际比赛的进程是完全一致的。它说,在比赛的任何时刻,当裁判念到某张任务卡 $A_{pq}$ 时,当前正在准备起跑的选手,一定就站在 $p$ 区的起跑线上。这个断言确保了我们对比赛的规划(文法推导)和比赛的实际执行(DPDA 计算)之间没有偏差。

22.3 证明

📜 [原文8]

证明采用关于 $i$(从 $A_{q_0, q_{\text{accept}}}$ 推导 $v_i$ 的步骤数)的归纳法

基础情况$i=0$

在这种情况下,$v_i=A_{q_0, q_{\text{accept}}}$ 并且 $P$ 从状态 $q_0$ 开始,因此基础情况成立。

归纳步骤:假设对 $i$ 断言成立,并证明对 $i+1$ 成立。

首先考虑情况 $v_i=x A_{pq} y$ 并且 $A_{pq}$ 是在步骤 $v_i \Rightarrow v_{i+1}$ 中被替换的变量归纳假设意味着 $P$ 在读取 $x$ 之后,读取符号 $A_{pq}$ 之前进入状态 $p$。根据 $G$ 的构造,替换规则可以是两种类型:

  1. $A_{pq} \rightarrow A_{pr} a A_{st} b$
  2. $A_{pp} \rightarrow \varepsilon$

因此,根据使用哪种类型的规则,$v_{i+1}=x A_{pr} a A_{st} b y$$v_{i+1}=xy$。在第一种情况下,当 $P$ 读取 $v_{i+1}$ 中的 $A_{pr} a A_{st} b$ 时,我们知道它从状态 $p$ 开始,因为它刚刚完成读取 $x$。当 $P$ 读取 $v_{i+1}$ 中的 $A_{pr} a A_{st} b$ 时,由于替换规则的构造,它将按顺序进入状态 $r, s, t$$q$。因此,它在读取 $A_{pr}$ 之前进入状态 $p$,并在读取 $A_{st}$ 之前进入状态 $s$,从而确立了对这两个变量出现的断言断言$y$ 部分的变量出现上成立,因为在 $P$ 读取 $b$ 之后,它进入状态 $q$,然后它读取字符串 $y$。在输入 $v_i$ 上,它也在读取 $y$ 之前进入 $q$,因此计算在 $v_i$$v_{i+1}$$y$ 部分上一致。显然,计算在 $x$ 部分上一致。因此,断言$v_{i+1}$ 成立。在第二种情况下,没有引入新变量,所以我们只需观察计算在 $v_i$$v_{i+1}$$x$$y$ 部分上一致。这证明了断言

📖 [逐步解释]

这部分是断言 2.60 的形式化证明,使用了标准的数学归纳法

  1. 证明方法:数学归纳法
    • 我们要证明的命题 P(i) 是:“对于推导步骤数 $i$,在句型 $v_i$ 中出现的任何变量 $A_{pq}$,当 DPDA $P$ 的计算到达它时,P 的状态为 $p$。”
    • 归纳法需要两步:
    • 基础情况 (Base Case): 证明 P(0) 成立。
    • 归纳步骤 (Inductive Step): 假设 P(i) 成立(这被称为归纳假设),然后利用这个假设来证明 P(i+1) 也成立。
  2. 基础情况 (i=0)
    • $i=0$ 时,推导还没开始,句型是 $v_0 = A_{q_0, q_{\text{accept}}}$
    • DPDA $P$ 的计算也还没开始,它处于初始状态 $q_0$
    • $P$ 即将“读取”的变量是 $A_{q_0, q_{\text{accept}}}$,这个变量的第一个下标是 $q_0$
    • $P$ 的当前状态 ($q_0$) 与变量的第一个下标 ($q_0$) 完全匹配。
    • 因此,断言在 $i=0$ 时成立。
  3. 归纳步骤 (假设 P(i) 成立,证明 P(i+1) 成立)
    • 归纳假设: 我们假设在第 $i$ 步的句型 $v_i$ 中,断言是成立的。
    • 考虑从 $v_i$$v_{i+1}$ 的一步推导。这意味着 $v_i$ 中的某个变量被它的产生式右侧替换了。
    • $v_i = x A_{pq} y$,其中 $A_{pq}$ 是被替换的那个变量。$x$$y$$A_{pq}$ 前后的字符串片段。
    • 根据归纳假设,当 DPDA $P$ 读完 $x$ 后、准备读 $A_{pq}$ 前,它的状态是 $p$
  4. 分情况讨论替换规则
    • DPDA 构造 CFG 的算法会产生两种主要类型的规则(这里为了简化,只列出了其中一种复杂形式和一种简单形式的代表):
    • 类型1 (复杂情况): $A_{pq} \rightarrow A_{pr} a A_{st} b$。这条规则对应 DPDA 的一系列动作:从 $p$$A_{pr}$$r$,再读 $a$$s$,再读 $A_{st}$$t$,再读 $b$$q$
    • 类型2 (简单情况): $A_{pp} \rightarrow \varepsilon$。这条规则对应 DPDA 在状态 $p$ 不动,栈也不变的情况。
    • 情况A: 应用类型1规则
    • $v_{i+1}$ 变成 $x (A_{pr} a A_{st} b) y$
    • 我们来分析 DPDA $P$ “读取” $v_{i+1}$ 的过程:
    • 对于 x 部分: $P$ 读取 $x$ 的行为和它读取 $v_i$$x$ 部分时完全一样。因此,对于 $x$ 中可能存在的任何变量,断言仍然成立。
    • 对于 $A_{pr}$: 读完 $x$ 后,$P$ 的状态是 $p$ (根据归纳假设)。现在它要读 $A_{pr}$$A_{pr}$ 的第一个下标是 $p$$P$ 的当前状态与变量下标匹配。断言对 $A_{pr}$ 成立。
    • 对于 $A_{st}$: 根据规则的构造,$P$$p$ “读”完 $A_{pr}$ 会到 $r$,再读 a 会到 $s$。所以,在准备读 $A_{st}$ 之前,$P$ 的状态恰好是 $s$$A_{st}$ 的第一个下标是 $s$$P$ 的当前状态与变量下标匹配。断言对 $A_{st}$ 成立。
    • 对于 y 部分: $P$ 在读完 $A_{pr} a A_{st} b$ 这一整块后,状态会从 $p$ 变为 $q$。之后它开始读取 $y$。这和在 $v_i$ 中,$P$ 读完 $A_{pq}$ 后状态变为 $q$ 再读取 $y$ 的情况完全一致。因此,对于 $y$ 中可能存在的任何变量,断言也成立。
    • 结论:在情况A中,断言对 $v_{i+1}$ 成立。
    • 情况B: 应用类型2规则
    • $v_{i+1}$ 变成 $xy$
    • 变量 $A_{pp}$ε 替换,消失了,没有引入新的变量。
    • $P$ 读取 $v_{i+1}$ 的行为,就是在读完 $x$ 之后直接读取 $y$
    • $v_i$ 中,P 读完 $x$ 到达状态 $p$,然后“读取”$A_{pp}$,状态不变还是 $p$,然后再读取 $y$
    • $v_{i+1}$ 中,P 读完 $x$ 到达状态 $p$,然后直接读取 $y$
    • 两种情况下,读取 $x$$y$ 的上下文(起始状态)完全一致。既然断言在 $v_i$$x$$y$ 中成立,那么在 $v_{i+1}$ 中也必然成立。
    • 结论:在情况B中,断言对 $v_{i+1}$ 成立。
  5. 归纳法结论
    • 我们已经证明了基础情况成立,并且如果断言对第 $i$ 步成立,那么它对第 $i+1$ 步也成立。
    • 根据数学归纳法原理,该断言对所有 $i \ge 0$ 都成立。证明完毕。
💡 [数值示例]
  • 回顾之前的例子:
  • 推导步骤: $v_i = a A_{q_1 q_2} b \Rightarrow a c A_{q_3 q_2} d b = v_{i+1}$
  • 规则: $A_{q_1 q_2} \rightarrow c A_{q_3 q_2} d$
  • 归纳假设: 在 $v_i$ 中,当 DPDA 读完 a 时,状态为 $q_1$
  • 证明 $v_{i+1}$:
  • DPDA$v_{i+1}$:
  • a,状态从 $q_0$ 变为 $q_1$
  • 现在要读 c。因为 $A_{q_1 q_2} \rightarrow c \dots$ 这个规则的来源就是 DPDA 在状态 $q_1$c 的转移,所以读完 c 后,DPDA 的状态必然会是我们构造规则时所用的那个中间状态,也就是 $A_{q_3 q_2}$ 的第一个下标 $q_3$
  • 此时,DPDA 准备读 $A_{q_3 q_2}$,而它的状态正好是 $q_3$。断言对于 $v_{i+1}$ 中的这个新变量成立。
  • 这个具体的演算过程,就是归纳步骤中抽象逻辑的实例化。
⚠️ [易错点]
  1. 易错点: 忽略了对 $x$$y$ 部分的讨论。证明的严谨性在于,不仅要证明新引入的变量满足断言,还要说明原有的、未被替换部分的变量也继续满足断言。
  2. 易错点: 对 PDA->CFG 的构造规则不熟悉。整个证明都建立在“规则是如何从 DPDA 的转移中被构造出来的”这一基础上。如果不知道规则的来源,就无法理解为什么状态能完美匹配。
📝 [总结]

该证明通过严谨的数学归纳法,论证了断言 2.60 的正确性。它从最简单的基础情况 ($i=0$) 出发,然后假设在第 $i$ 步断言成立,通过分析一步推导可能带来的两种变化(变量被复杂结构替换或被 ε 替换),证明了在第 $i+1$ 步,所有变量(无论是新来的还是原有的)与其对应的 DPDA 运行时状态之间的同步关系依然保持。

🎯 [存在目的]

本段的存在是为了给断言 2.60 提供严格的、无可辩驳的数学支持。在理论计算机科学中,一个“断言”或“引理”若无证明,就只是一个猜想。这个证明是后续所有依赖于该断言的论证(特别是断言 2.61)能够成立的逻辑基石。

🧠 [直觉心智模型]

这就像证明一个递归程序的正确性。

  1. 断言:程序对于任何输入都返回正确结果。
  2. 基础情况:证明程序对于最简单的输入(例如 0 或空列表)是正确的。
  3. 归纳步骤:假设程序对于输入 n 是正确的,然后证明它对于输入 n+1 也是正确的。这里的 v_i -> v_{i+1} 就好比是从 nn+1 的一步计算,证明过程就是分析这一步计算如何维持了程序的“正确性”这个不变量。
💭 [直观想象]

想象在玩多米诺骨牌。

  1. 断言: 所有骨牌最终都会倒下。
  2. 基础情况 (i=0): 我们推倒第一张骨牌 ($v_0 = A_{q_0, q_{\text{accept}}}$)。证明:我们确实推了它,所以它倒了。
  3. 归纳步骤:
  4. 归纳假设: 假设第 $i$ 张骨牌会倒下。
  5. 证明: 我们需要证明,第 $i$ 张骨牌倒下时,一定会撞倒第 $i+1$ 张骨牌。
  6. 这里的“一步推导” $v_i \Rightarrow v_{i+1}$ 就好比是第 $i$ 张牌和第 $i+1$ 张牌之间的物理联系。证明过程就是去检查这个联系:
  7. 情况A(复杂规则):第 $i$ 张牌倒下后,会触发一个杠杆装置,该装置会精确地推倒第 $i+1$ 张牌。我们分析这个装置的每个环节,确保力能传递过去。
  8. 情况B(ε规则):第 $i$ 张牌和第 $i+1$ 张牌其实是粘在一起的,第 $i$ 张倒了,第 $i+1$ 张必然跟着倒。
  9. 结论: 既然第一张倒了,并且每一张倒下都会触发下一张倒下,那么所有骨牌都会倒下。断言成立。

2.2.4 断言 2.61

📜 [原文9]

$G$ 通过 DK 检验

📖 [逐步解释]

这是证明 DPDA P 等价于 DCFG G 的核心步骤的最终宣言。它宣称,我们通过一系列精巧构造和修正得到的文法 $G$,最终是可以通过确定性的“年审”——DK 检验的。

  1. 什么是 DK 检验?
    • DK 检验是一种用于判断一个 CFG 是否为 DCFG (即 LR(0) 文法) 的算法。
    • 它首先从文法 $G$ 构造一个 NFA (我们称之为 $K$),这个 NFA 的状态是“带点规则 (dotted rules)”,例如 $A \rightarrow u \cdot v$
    • 然后,通过子集构造法将这个 NFA $K$ 转换为一个 DFA,我们称之为 $DK$$DK$ 的每个状态都是一个带点规则的集合
    • 检验的核心内容: 检查 $DK$ 的所有状态。如果没有任何一个状态同时包含以下两种冲突情况,那么文法就通过 DK 检验
    • 规约/规约冲突 (reduce/reduce conflict): 状态中包含两个或更多已完成的规则(点在最末尾),例如 $\{ A \rightarrow u \cdot, B \rightarrow v \cdot \}$。这会导致解析器不知道是该按 $A \rightarrow u$ 规约,还是按 $B \rightarrow v$ 规约。
    • 移入/规约冲突 (shift/reduce conflict): 状态中既包含一个已完成的规则 ($A \rightarrow u \cdot$),又包含一个点后面跟着终结符的未完成规则 ($B \rightarrow v \cdot a w$)。这会导致解析器困惑:是应该立即按 $A \rightarrow u$ 进行规约,还是应该移入下一个输入符号 a
    • DCFG 的定义: 一个文法是 DCFG,当且仅当它通过 DK 检验
  2. 断言的意义
    • 断言 2.61 直接声称我们构造的文法 $G$ 满足上述检验条件,即在它的 $DK$ 自动机的任何状态里,都不会出现规约/规约冲突或移入/规约冲突。
    • 一旦这个断言被证明,根据 DCFG 的定义,我们就证明了 $G$ 是一个 DCFG
    • 这就完成了引理 2.59 的证明,从而完成了定理 2.57 的半壁江山。
📝 [总结]

断言 2.61 是一个结论性的声明,它宣称我们从 DPDA 精心构造出的文法 $G$,最终满足了作为确定性上下文无关文法 (DCFG) 的严格判定标准——DK 检验。证明这个断言是整个引理证明的终点线。

🎯 [存在目的]

这个断言的存在,是为了给整个 DPDA -> DCFG 的构造过程一个最终的“合格认证”。前面的所有工作——改造 DPDA、应用通用构造、修复歧义、建立断言 2.60 的对应关系——都是在为证明这个最终断言铺路。它将前面所有铺垫联系在一起,得出了我们想要的最终结论:我们构造出的文法是确定性的。

🧠 [直觉心智模型]

这就像一个学生经过漫长的学习和准备后,最终宣称:“我能通过期末考试(DK 检验)!”

  1. 学生: 我们构造的文法 $G$
  2. 学习过程: 从 DPDA 构造 $G$ 并修正它的过程。
  3. 知识点掌握 (断言 2.60): 学生已能将每个知识点(文法规则)与老师的讲解(DPDA 行为)对应起来。
  4. 期末考试 (DK 检验): 一套标准化的试卷,测试学生在任何情况下是否会产生知识点的混淆(冲突)。
  5. 断言 2.61: 就是学生充满信心的宣言。接下来的证明,就是他展示如何在考卷的每一道题上(分析每一种可能的冲突),利用他所学的知识,证明自己绝不会答错。
💭 [直观想象]

回到考古学家发现织布机的比喻。

  1. 考古学家: 证明者。
  2. 修正后的图纸: 文法 $G$
  3. 皇家纺织工艺标准: DK 检验
  4. 断言 2.61: 考古学家在仔细研究了图纸后,向皇家学会宣布:“我发现的这张古代图纸,完全符合《皇家纺织工艺标准》!”
  5. 接下来的证明,就是考古学家拿出标准文献的每一条(DK 检验的每个条件),逐一与图纸对照,向所有人展示图纸的设计($G$ 的结构)是如何完美地避免了所有标准中禁止的“劣质工艺”(冲突)的。

22.5 证明

📜 [原文10]

我们证明 $DK$ 的每个接受状态都满足 DK 检验要求。选择这些接受状态之一。它包含一个已完成规则 $R$。这个已完成规则可能有两种形式:

  1. $A_{pq} \rightarrow A_{pr} a A_{st} b.$
  2. $A_{pp} \rightarrow .$

在这两种情况下,我们都需要证明接受状态不能包含

a. 另一个已完成规则,并且

b. 一个点后立即跟着终结符号的带点规则

我们分别考虑这四种情况。在每种情况下,我们首先考虑一个字符串 $z$$DK$ 在其上进入我们上面选择的接受状态。

情况 1a。这里 $R$ 是一个已完成的类型 1-2 规则。对于此接受状态中的任何规则,$z$ 必须以该规则中点之前的符号结尾,因为 $DK$$z$ 上进入该状态。因此,点之前的符号在所有此类规则中必须一致。这些符号在 $R$ 中是 $A_{pr} a A_{st} b$,因此任何其他类型 1-2 的已完成规则都必须在右侧具有完全相同的符号。由此可知,左侧的变量也必须一致,因此规则必须相同。

假设接受状态包含 $R$ 和某个类型 3 已完成 $\varepsilon$ 规则 $T$。从 $R$ 我们知道 $z$$A_{pr} a A_{st} b$ 结尾。此外,我们知道 $P$$z$ 的末尾弹出其栈,因为在 $R$ 中在该点发生了弹出,这是由于 $G$ 的构造。根据我们构建 $DK$ 的方式,状态中已完成的 $\varepsilon$ 规则必须派生自位于同一状态的带点规则,其中点不在开头,并且点紧接在某个变量之前。(在 $DK$ 的开始状态中可能出现异常,其中点可能出现在规则的开头,但此接受状态不能是开始状态,因为它包含一个已完成的类型 1-2 规则。)在 $G$ 中,这意味着 $T$ 派生自一个类型 1-2 的带点规则,其中点位于第二个变量之前。从 $G$ 的构造来看,在点之前发生了一个压入操作。这意味着 $P$$z$ 的末尾进行了一次压入移动,这与我们之前的陈述相矛盾。因此,已完成的 $\varepsilon$ 规则 $T$ 不可能存在。无论哪种情况,此接受状态中不能出现第二个已完成规则。

情况 2a。这里 $R$ 是一个已完成的 $\varepsilon$ 规则 $A_{pp} \rightarrow .$。我们证明没有其他已完成的 $\varepsilon$ 规则 $A_{qq} \rightarrow .$ 可以与 $R$ 共存。如果共存,前面的断言表明 $P$ 在读取 $z$ 后必须处于 $p$ 状态,并且在读取 $z$ 后也必须处于 $q$ 状态。因此 $p=q$,并且这两个已完成的 $\boldsymbol{\varepsilon}$ 规则是相同的。

情况 1b。这里 $R$ 是一个已完成的类型 1-2 规则。从情况 1a 我们知道 $P$$z$ 的末尾弹出其栈。假设接受状态也包含一个带点规则 $T$,其点后立即跟着终结符号。从 $T$ 我们知道 $P$$z$ 的末尾不弹出其栈。这个矛盾表明这种情况不可能发生。

情况 2b。这里 $R$ 是一个已完成的 $\varepsilon$ 规则。假设接受状态还包含一个带点规则 $T$,其点后立即跟着终结符号。因为 $T$ 是类型 1-2,变量符号紧接在点之前,因此 $z$ 以该变量符号结尾。此外,在 $P$ 读取 $z$ 后,它已准备好读取一个非 $\varepsilon$ 输入符号,因为终结符号紧接在点之后。如情况 1a 所示,已完成的 $\varepsilon$ 规则 $R$ 派生自类型 1-2 的带点规则 $S$,其中点紧接在第二个变量之前。(同样,此接受状态不能是 $DK$ 的开始状态,因为点不出现在 $T$ 的开头。)因此,某个符号 $\hat{a} \in \Sigma_{\varepsilon}$ 紧接在 $S$ 中的点之前,因此 $z$$\hat{a}$ 结尾。$\hat{a} \in \Sigma$$\hat{a}=\varepsilon$,但由于 $z$变量符号结尾,$\hat{a} \notin \Sigma$,因此 $\hat{a}=\varepsilon$。因此,在 $P$ 读取 $z$ 但在它进行 $\varepsilon$ 输入移动以处理 $\hat{a}$ 之前,它已准备好读取一个 $\varepsilon$ 输入。我们上面还证明了 $P$ 在这一点已准备好读取一个非 $\varepsilon$ 输入符号。但是,DPDA 不允许在给定状态和栈的情况下同时进行 $\varepsilon$ 输入移动和读取非 $\varepsilon$ 输入符号的移动,因此上述情况是不可能的。因此,这种情况不可能发生。

📖 [逐步解释]

这部分是断言 2.61 的核心证明,采用的是反证法分类讨论。其目标是证明在 $DK$ 自动机的任何一个状态里,都不会出现冲突。证明的焦点集中在接受状态,因为根据 DK 检验的定义,冲突只可能发生在包含已完成规则(即准备规约)的状态中。

证明策略

  1. 任选一个 $DK$ 的状态,假设它包含一个已完成规则 $R$
  2. 我们将证明这个状态里不可能有任何其他规则与之形成冲突。
  3. 冲突有两种:规约/规约冲突(另一个已完成规则)和移入/规约冲突(一个点后是终结符的未完成规则)。
  4. 同时,已完成规则 $R$ 本身也分为两种类型,一种是复杂的(来自 DPDA 的一系列动作),一种是简单的(来自 ε 规则)。
  5. 这就构成了 2 x 2 = 4 种需要排除的情况。

情况 1a: 复杂规约 vs. 任何其他规约 (排除 Reduce/Reduce 冲突)


情况 2a: ε-规约 vs. ε-规约 (排除 Reduce/Reduce 冲突)


情况 1b: 复杂规约 vs. 移入 (排除 Shift/Reduce 冲突)


情况 2b: ε-规约 vs. 移入 (排除 Shift/Reduce 冲突)


📝 [总结]

该证明通过对四种可能的冲突情况进行逐一排查,并利用反证法,成功证明了我们构造的文法 $G$ 不会产生任何冲突。每一种情况的排除都最终归结于利用了源 DPDA $P$确定性

  1. 一个 DPDA 在同一时刻、同一状态,其后续动作是唯一的。
  2. 它不能既做 ε 转移又做非 ε 转移。
  3. 它不能既压栈又弹栈。
  4. 它到达的状态是唯一的。

由于文法 $G$ 是对 DPDA $P$ 行为的忠实编码,因此 $P$ 的确定性就直接传导给了 $G$,保证了 $G$ 能通过 DK 检验

🎯 [存在目的]

本段是整个引理 2.59 证明的收官之战。它的目的就是以一种滴水不漏的逻辑,完成对断言 2.61 的证明。通过这个详尽的分类讨论,它将抽象的“等价性”问题,转化为了对具体冲突场景的分析,并利用 DPDA 的基本定义来解决这些问题,展示了理论证明的严谨与力量。

🧠 [直觉心智模型]

这就像一个侦探在排除嫌疑。

  1. 案发现场: 一个 $DK$ 状态。
  2. 一个明确的线索: 存在一个已完成规则 $R$(规约)。
  3. 侦探的目标: 证明现场没有其他任何可疑人员或可疑活动(没有其他冲突规则)。
  4. 情况 1a: 现场有另一个尸体(另一个规约)。侦探通过 DNA 比对(规则一致性论证)发现,这其实是同一个人的不同照片,不是两个尸体。
  5. 情况 2a: 现场有两个鬼影(ε规约)。侦探通过时空坐标分析(断言 2.60)发现,两个鬼影的坐标完全重合,所以其实是同一个鬼影。
  6. 情况 1b/2b: 现场既有尸体(规约),又有人在撬门(移入)。侦探知道,根据物理定律(DPDA 确定性),一个空间不能同时发生两件互斥的事。因此这种场景不可能存在。
  7. 结论: 现场是干净的,没有冲突。
💭 [直观想象]

想象一个十字路口的交通信号灯系统($DK$ 状态)。

  1. 一个已完成规则 $R$: 就像是南北方向的绿灯亮了,允许车辆“通过”(规约)。
  2. 目标: 证明这个系统是安全的(无冲突)。
  3. 情况 1a (R/R 冲突): 能否南北向绿灯亮的同时,另一个方向(比如西北-东南方向)的绿灯也亮?不行,城市交通法(DPDA 确定性)不允许,一个路口在一个相位只能有一个主流向的绿灯。
  4. 情况 1b (S/R 冲突): 能否南北向绿灯亮的同时,东西方向的“左转”箭头灯也亮(移入)?不行,这会造成直行和左转车辆的交织,交通法不允许。
  5. 情况 2b (ε-R/S 冲突): 能否南北向的行人“允许通行”信号(ε 移动)亮的同时,东西方向的机动车绿灯也亮?绝对不行,这是最危险的冲突。DPDA 的设计从根本上杜绝了这种情况。
  6. 结论: 这个交通信号灯系统的设计(文法 $G$ 的构造)是安全的,因为它完全基于一个无冲突的底层逻辑(DPDA 的确定性)。

33. 解析和 LR(K) 文法

📜 [原文11]

确定性上下文无关语言具有重要的实际意义。它们的成员资格和解析算法基于 DPDA,因此效率高,并且它们包含丰富的 CFL 类别,其中包括大多数编程语言。然而,DCFG 有时不便于表达特定的 DCFL。所有句柄都必须是强制的要求通常是设计直观 DCFG 的障碍。

幸运的是,一个更广泛的文法类别,称为 LR(k) 文法,为我们提供了两全其美的优点。它们与 DCFG 足够接近,可以直接转换为 DPDA。然而,它们对于许多应用来说表达能力足够强。

LR(k) 文法的算法引入了前瞻 (lookahead)。在 DCFG 中,所有句柄都是强制的。句柄仅取决于有效字符串中(包括句柄本身)的符号,而不取决于句柄后面的终结符号。在 LR(k) 文法中,句柄也可能取决于句柄后面的符号,但仅限于这些符号的前 $k$ 个。缩写 LR(k) 代表:从左到右的输入处理 (Left to right input processing)、最右推导(或等价地,最左归约)(Rightmost derivations),以及 $k$前瞻符号 (k symbols of lookahead)。

为了使其精确,设 $h$有效字符串 $v=xhy$句柄。我们说 $h$前瞻 $k$ 强制,如果 $h$ 是每个有效字符串 $x h \hat{y}$ 的唯一句柄,其中 $\hat{y} \in \Sigma^{*}$,并且 $y$$\hat{y}$ 在它们的前 $k$ 个符号上一致。(如果任一字符串的长度小于 $k$,则这些字符串必须一致到较短字符串的长度。)

📖 [逐步解释]

这一部分将理论从纯粹的 DCFG 拓展到了一个在实践中更有用、更强大的概念——LR(k) 文法

  1. 动机:DCFG 的局限性
    • DCFL 非常重要,因为它们可以用高效的 DPDA 解析,而大多数编程语言的语法都属于 DCFL
    • 我们刚刚证明了 DCFG 在有结束标记时可以描述所有 DCFL。但理论上可以,不代表实践中好用。
    • DCFG (也就是 LR(0) 文法) 的一个核心要求是“句柄是强制的”。这意味着解析器在决定是否规约时,完全不能看它还没读到的任何输入符号。它仅仅根据已经读过的内容,就必须做出唯一的决定。
    • 这个要求太严格了,导致用 DCFG 来写一些直观的语法变得非常困难和反直觉。
  2. 解决方案:引入前瞻 (Lookahead)
    • LR(k) 文法 放松了这个严格的限制。它允许解析器在做决定前,偷偷地“向前看 k 个”还未处理的输入符号。
    • 这个小小的“偷看”能力,极大地增强了文法的表达能力和自然性。
    • LR(k) 的含义拆解:
    • L (Left to right): 输入是从左到右处理的。
    • R (Rightmost derivation): 文法对应的是一个最右推导的逆过程(即最左规约),这正是我们之前讨论的移入-规约解析的特点。
    • k (k symbols of lookahead): 决定是否规约以及如何规约时,最多可以参考句柄之后(即输入流中尚未读取的)$k$ 个终结符。
  3. DCFG 与 LR(0) 的关系
    • DCFG 就是 LR(0) 文法$k=0$ 意味着完全没有前瞻能力。解析器做决定时是个“睁眼瞎”,只看自己已经处理过的东西。
  4. 前瞻强制句柄的精确定义
    • 假设我们有一个可能的句柄 $h$,它出现在字符串 $xhy$ 中。
    • 我们说这个句柄 $h$ 是“由前瞻 k 强制的”,意思是:
    • 不仅要考虑当前的字符串 $y$ (句柄后面的部分),还要考虑所有可能的、以与 $y$$k$ 个符号相同的方式开头的其他字符串 $\hat{y}$
    • 在所有这些可能的未来($xh\hat{y}$)中,$h$ 都必须是唯一的、正确的句柄。
    • 换句话说,只要句柄后面 $k$ 个符号不变,我们的规约决策就不会改变。
💡 [数值示例]
  • 示例1: 著名的 if-then-else 语法
  • 考虑一条简化的语法规则:
  • stmt -> 'if' expr 'then' stmt
  • stmt -> 'if' expr 'then' stmt 'else' stmt
  • 当解析器看到 if expr then stmt 这一段时,它面临一个 移入/规约冲突
  • 规约?: 如果后面没有 else,那么 if expr then stmt 就是一个完整的 stmt,应该按第一条规则规约。
  • 移入?: 如果后面紧跟着一个 else,那么解析器不应该规约,而应该继续移入 else,以匹配第二条规则。
  • 这是一个典型的 LR(0) (即 DCFG) 无法解决的问题,因为它需要前瞻
  • 对于一个 LR(1) 解析器,它会向前看 1 个符号。当它看到 if expr then stmt 后:
  • 如果下一个符号是 else,它就选择移入
  • 如果下一个符号是其他任何东西(比如分号 ; 或者文件结尾 \dashv),它就选择规约
  • 通过1个符号的前瞻,冲突解决了。所以这个文法不是 LR(0) 的,但是 LR(1) 的。
  • 示例2: C语言中的类型名与变量名
  • 在C语言中,typedef int my_type; 定义了一个新的类型名 my_type
  • 之后,my_type x;my_type(0); 这两种写法。
  • my_type x; 中,my_type 是一个类型说明符。
  • my_type(0); 中,my_type 被用作一个类型转换函数。
  • 当解析器读到 my_type 时,它不知道该如何解释它。它需要前瞻
  • 如果下一个符号是 (, 那么这很可能是一个类型转换。
  • 如果下一个符号是一个标识符(如 x),那么这很可能是一个变量声明。
  • 这显示了为什么实际的编程语言解析器几乎总是需要至少 $k=1$ 的前瞻能力。
⚠️ [易错点]
  1. 易错点: 认为 LR(k) 语言比 LR(0) 语言更强大。这是错误的。对于语言的识别能力来说,LR(k) 文法 (对于所有 $k \ge 0$) 生成的语言集合都是完全相同的,都是 DCFLLR(k) 的优势在于文法的表达能力。同一个 DCFL,用 LR(1) 文法来写可能非常自然,而用 LR(0) (DCFG) 来写可能极其复杂和反人类。
  2. 边界情况: $k$ 的值。在理论上 $k$ 可以是任何非负整数。但在实践中,绝大多数编程语言都可以用 LR(1) 文法来描述。需要 $k>1$ 的情况非常罕见。YACC 和 Bison 等工具主要就是构建 LALR(1) 解析器,它是 LR(1) 的一个稍弱但更高效的变体。
📝 [总结]

本段从 DCFG 的实践局限性出发,引入了更强大、更实用的 LR(k) 文法。其核心优势在于允许解析器在做决策时“向前看 $k$ 个符号”(前瞻)。这解决了 DCFG (即 LR(0)) 中常见的“移入/规约冲突”,使得语法规则可以写得更自然、更直观,同时又不牺牲语言的确定性。最后,本段给出了前瞻的精确定义,为后续讨论奠定了基础。

🎯 [存在目的]

本段的目的是将理论与实践联系起来。在证明了 DCFGDPDA 的理论等价性后,它指出了 DCFG 在实际应用中的不足,从而引出 LR(k) 文法这一在编译器设计领域至关重要的概念。它解释了为什么我们需要比 LR(0) 更强的文法,并定义了这种文法的核心特征——前瞻。这为接下来介绍 LR(1)DK1 检验以及证明所有 LR(k) 文法都描述 DCFL 做了铺垫。

🧠 [直觉心智模型]
  1. DCFG / LR(0) 解析器: 一个只能看到后视镜开车的司机。他只能根据已经开过的路来决定现在是该转弯(规约)还是继续直行(移入)。
  2. LR(k) 解析器: 一个不仅能看后视镜,还能透过挡风玻璃看到前方 $k$ 个路标的司机。当他到达一个岔路口(决策点)时,如果他看到前方 100米处有个“左转进入高速”的路标(前瞻符号),他就会选择走左边的车道(移入);如果前方是“道路尽头”的路标,他就会选择停车(规约)。这种能力让他开车更从容、更智能。
💭 [直观想象]

你在下棋。

  1. LR(0) 棋手: 他只考虑当前棋盘的局面,如果某个局面满足了某个“必杀定式”的条件,他就立刻执行这个定式(规约),完全不考虑对手下一步可能怎么走。
  2. LR(k) 棋手: 他不仅看当前局面,还会推演对手接下来可能的 $k$ 步棋(前瞻)。当他发现一个看似可以“必杀”的局面时,他会先想:“如果我这么走,对手下一步如果走这里(前瞻符号1),我会被反杀;但如果他走那里(前瞻符号2),我就赢定了。” 根据对未来的推演,他做出当前最有利的决策。
  3. LR(k) 文法就是赋予了解析器这种“深思熟虑”、“预判未来”的能力。

3.1 定义 2.62

📜 [原文12]

LR(k) 文法是一种上下文无关文法,其中每个有效字符串句柄都由前瞻 $k$ 强制

因此,DCFGLR(0) 文法相同。我们可以证明对于每个 $k$,我们可以将 LR(k) 文法转换为 DPDA。我们已经证明 DPDA 等价于 LR(0) 文法。因此 LR(k) 文法对于所有 $k$ 在能力上是等价的,并且都准确描述了 DCFL。以下示例表明 LR(1) 文法DCFG 更方便指定某些语言。

📖 [逐步解释]

这部分给出了 LR(k) 文法的正式定义,并立即阐明了它在计算能力谱系中的位置。

  1. 正式定义:
    • LR(k) 文法首先是一个上下文无关文法
    • 它的核心特性是:对于任何一个合法的中间推导形式(有效字符串),其句柄(下一个要被规约的部分)都是由“前瞻 k 强制”的。
    • 前瞻 k 强制”的含义是,只要句柄后面跟着的 $k$ 个输入符号是确定的,那么关于这个句柄的解析决策(是规约?按哪个规则规约?)就是唯一的。
  2. 重要推论:
    • DCFG = LR(0): DCFG 的定义是句柄必须是“强制”的,不依赖任何前瞻。这正好是 $k=0$ 的情况。所以,DCFGLR(0) 文法是同一个概念的两种不同叫法。
    • LR(k) 文法描述的能力范围:
    • LR(k) → DPDA: 我们可以证明(后面会展示思路),任何一个 LR(k) 文法都能被转换成一个等价的 DPDA。这意味着 LR(k) 生成的语言最多就是 DCFL
    • DPDA → LR(0): 我们已经证明(引理 2.59),任何一个识别带结束标记 DCFLDPDA 都能被转换成一个等价的 DCFG,也就是 LR(0) 文法。
    • 结论: 结合这两点,形成了一个闭环:DPDA 描述的能力范围是 DCFLLR(k) 能被 DPDA 识别,而 DPDA 能被 LR(0) 生成。这意味着,对于 $k=0, 1, 2, \dots$,所有 LR(k) 文法生成的语言集合都是完全相同的,这个集合不多不少,正好就是确定性上下文无关语言 (DCFL) 的集合。
  3. 能力 vs. 便利性:
    • 能力上等价: 在“能生成哪些语言”这个问题上,LR(0), LR(1), LR(2), ... 都没区别,它们的能力上限都是 DCFL
    • 便利性上不同: 但在“写出一个文法来描述某个特定 DCFL 有多方便”这个问题上,它们差别巨大。LR(1)LR(0) (DCFG) 要方便得多、自然得多。后面的例子将会证明这一点。
💡 [数值示例]
  • 示例: 理解能力等价
  • 考虑语言 $L = \{ a^n b \mid n \ge 1 \} \cup \{ a^n c \mid n \ge 1 \}$
  • 用 LR(1) 文法描述:
  • $S \rightarrow a A$
  • $A \rightarrow a A \mid b \mid c$ (这个文法有歧义,换一个)
  • $S \rightarrow A b \mid B c$
  • $A \rightarrow a A \mid a$
  • $B \rightarrow a B \mid a$ (这个也不好,太复杂)
  • 一个更合适的 LR(1) 文法:
  • $S \rightarrow aX$
  • $X \rightarrow aX \mid Y$
  • $Y \rightarrow b \mid c$

当解析器看到一串 a 之后,它需要决定是继续移入 a 还是规约。比如对于 aaYY 是句柄,但规约为 $X$ 还是继续移入?这需要前瞻。实际上,这个语言是正则的,但我们用它来思考。

  • 让我们用一个更经典的例子:$L = \{a^n b^n \mid n \ge 0\} \cup \{a^n c^{2n} \mid n \ge 0\}$。这个语言不是 DCFL。
  • 回到 if-then-else 的例子。这个语法是 LR(1) 的。理论上,一定存在一个等价的、但是可能非常丑陋和反直觉的 LR(0) 文法(DCFG)也能生成同样的语言。例如,可能需要引入很多新的非终结符来“编码”前瞻信息,比如 Stmt_without_elseStmt_with_else 等,让文法变得非常臃肿。
  • 结论: LR(1)LR(0) 能描述同样的语言集合,但对于某个具体的语言,用 LR(1) 写的文法可能只有5条规则,而等价的 LR(0) 文法可能有20条规则。
⚠️ [易错点]
  1. 最大易错点: 再次强调,不要混淆 文法的表达能力 (hierarchy of grammars: LR(0) ⊂ LR(1) ⊂ LR(2) ...) 和 语言的生成能力 (power of language classes: L(LR(0)) = L(LR(1)) = L(LR(2)) = DCFL)。一个文法是 LR(1) 但不是 LR(0),但它生成的语言仍然是一个 DCFL,这个 DCFL 同样可以被一个(可能更复杂的)LR(0) 文法生成。
📝 [总结]

本段正式定义了 LR(k) 文法,并迅速阐明了它的核心理论地位:尽管 LR(k) 文法本身随着 $k$ 的增大而变得越来越强大(能以无歧义方式描述的语法结构越来越多),但它们所能生成的语言集合的边界是固定的,这个边界就是 DCFL。它进一步指出了 LR(k) 相比于 DCFG (LR(0)) 的主要优势不在于生成新类型的语言,而在于能以更简洁、更自然的方式描述已有的 DCFL

🎯 [存在目的]

本段的目的是在引入 LR(k) 的概念后,迅速消除一个潜在的误解,即认为 LR(k) 开创了一个全新的、比 DCFL 更大的语言类别。通过清晰地论证所有 LR(k) 文法在能力上都等价于 DPDA,本段将 LR(k) 牢牢地定位在 DCFL 的框架内。这使得我们可以将 LR(k) 视为解决 DCFG 实用性问题的“高级工具”,而不是一个全新的理论分支。

🧠 [直觉心智模型]

这就像比较不同的编程语言。

  1. DCFL: 就像是“所有可以用确定性算法在多项式时间内解决的问题”这个集合。
  2. LR(0) / DCFG: 像是一种非常低级、繁琐的编程语言,比如某种汇编。理论上,你可以用它解决上述集合里的所有问题,但写起代码来非常痛苦。
  3. LR(1) 文法: 像是一种高级语言,比如 Python。它提供了很多方便的语法糖和高级抽象。用 Python 写代码比用汇编要愉快得多。
  4. 结论: 尽管 Python 比汇编好写得多(便利性),但它们能解决的计算问题范围(能力)是相同的(都是图灵完备的)。同样,LR(1)LR(0) 好写,但它们能描述的语言范围都是 DCFL
💭 [直观想象]

想象你要写一本菜谱,来教人做各种菜。

  1. DCFL: 所有能用一套无歧义步骤做出来的菜。
  2. LR(0) 菜谱: 菜谱规定,每一步操作都必须是固定的,完全不能根据下一步要放什么调料来调整当前的操作。例如,它只能说“切 5 分钟”,而不能说“切到土豆丝变得半透明,如果下一步要炒就切粗点,如果下一步要凉拌就切细点”。
  3. LR(1) 菜谱: 菜谱允许你“看一眼”下一步要用的调料。例如,它会说:“开始切土豆丝...(此时向前看一步)...如果你看到下一步要用的是‘醋’,就把土豆丝切得细如发丝;如果你看到下一步要用的是‘油’,就切成筷子粗细。”
  4. 能力 vs. 便利性: 两种菜谱最终都能做出“炒土豆丝”和“凉拌土豆丝”这两道菜(能力等价),但显然第二种(LR(1))菜谱写起来更符合直觉,也更能指导厨师做出好菜(便利性更高)。

3.2 LR(1) 文法转换

📜 [原文13]

为避免繁琐的符号和技术细节,我们仅在 $k=1$ 的特殊情况下展示如何将 LR(k) 文法转换为 DPDA。一般情况下的转换方式基本相同。

📖 [逐步解释]

本段是一个过渡说明,旨在简化后续的讨论。

  1. 聚焦于 k=1: 作者决定,为了教学清晰,避免陷入复杂的通用符号表示,接下来的讨论将只关注 LR(1) 文法。
  2. LR(1) 的代表性: 这种情况($k=1$)是最重要、最普遍、最具代表性的。
    • 理论上: 掌握了从 LR(1)DPDA 的转换,就等于掌握了其核心思想。推广到任意 $k$ 只是符号上的复杂化,而非概念上的飞跃。
    • 实践上: 几乎所有的现代编程语言都可以用 LR(1) 文法来描述。现实世界中几乎用不到 $k>1$ 的情况。因此,深入理解 LR(1) 具有最高的实用价值。
  3. 转换的存在性: 本段重申了之前的一个结论:LR(k) 文法可以被转换为 DPDA。现在,作者准备展示这个转换的具体构造思路,但仅限于 $k=1$ 的情况。
📝 [总结]

本段是一个“范围说明”,它告诉读者,为了清晰和实用,接下来的技术细节将集中于将 LR(1) 文法转换为 DPDA 的过程,并指出这种方法可以推广到任意 $k$

🎯 [存在目的]

本段的目的是管理读者的预期,并使证明过程更易于理解。通过将问题简化为最重要和最常见的 LR(1) 情况,作者可以更清晰地展示构造的核心思想,而不会让读者淹没在繁琐的下标和复杂的集合定义中。这是一种常见的、有效的数学和计算机科学教学策略。

🧠 [直觉心智模型]

这就像在教人开车。老师会说:“为了简单起见,我们今天只学在晴天、白天、干燥路面上如何开车。学会了之后,晚上开车、雨天开车(对应 $k>1$ 的情况)的原理是相通的,只是需要多注意一些额外的细节。”

💭 [直观想象]

想象一位数学教授在讲授一个复杂的定理。他说:“这个定理在 n 维空间都成立。但为了让大家直观地理解,我们先在二维平面($k=1$ 的情况)上证明它。你们会看到,所有的核心思想都在二维情况下体现了。之后,推广到三维、四维乃至 n 维(一般情况 $k$),只是需要把所有的 $(x, y)$ 坐标换成 $(x_1, x_2, \dots, x_n)$ 而已,证明的逻辑是一样的。”


3.3 DK1 检验

📜 [原文14]

首先,我们将介绍 DK 检验的变体,为 LR(1) 文法进行了修改。我们称之为带前瞻 1 的 DK 检验,或简称为 $DK_1$ 检验。和以前一样,我们将构造一个 NFA,这里称为 $K_1$,并将其转换为 DFA $DK_1$$K_1$ 的每个状态都有一个带点规则 $T \rightarrow u . v$ 和一个终结符$a$,称为前瞻符号,表示为 $T \rightarrow u . v \quad a$。此状态表示 $K_1$ 最近读取了字符串 $u$,如果 $v$$u$ 之后,$a$$v$ 之后,那么 $u v$ 将是句柄的一部分。

形式构造与之前大致相同。开始状态对每个涉及开始变量 $S_1$ 的规则和每个 $a \in \Sigma$ 都有一个到 $S_{1 \rightarrow .ua}$$\varepsilon$ 移动。移进转移$T \rightarrow u . x v a$ 转换为 $T \rightarrow ux . va$,输入为 $x$,其中 $x$变量符号终结符号。$\varepsilon$ 转移将 $T \rightarrow u . Cva \quad a$ 转换为 $C \rightarrow . r \quad b$,对于每个规则 $C \rightarrow r$,其中 $b$ 是可以从 $v$ 派生的任何终结符串的第一个符号。如果 $v$ 派生 $\varepsilon$,则添加 $b=a$。接受状态是所有 $B \rightarrow u \cdot a$,用于已完成规则 $B \rightarrow u.$$a \in \Sigma$

$R_1$ 是一个已完成规则,前瞻符号$a_1$,设 $R_2$ 是一个带点规则前瞻符号$a_2$。如果满足以下条件,则称 $R_1$$R_2$ 是一致的:

  1. $R_2$ 已完成且 $a_1=a_2$,或
  2. $R_2$ 未完成且 $a_1$ 紧接在其点之后。

现在我们准备描述 $DK_1$ 检验。构造 DFA $DK_1$。检验规定每个接受状态不得包含任何两个一致的带点规则

📖 [逐步解释]

这部分描述了如何将用于 LR(0)DK 检验升级为用于 LR(1)$DK_1$ 检验。核心思想是在自动机的每个状态中,不仅记录句柄的解析进度,还附带记录了句柄后面应该跟什么符号。

  1. 核心变化:引入前瞻符号
    • 在原始的 DK 检验中,自动机的状态是带点规则,如 $A \rightarrow u \cdot v$
    • $DK_1$ 检验中,自动机的状态升级为“LR(1) 项”,形式为 $[T \rightarrow u \cdot v, a]$
    • $T \rightarrow u \cdot v$: 这部分和以前一样,表示我们期望识别一个 $T$,目前已经看到了 $u$,希望能看到 $v$
    • $a$: 这是新增的前瞻符号 (lookahead symbol)。它是一个终结符,代表我们期望在整个规则 $T \rightarrow uv$ 被成功规约后,输入流中紧接着出现的符号是 $a$
  2. 构造 $DK_1$ 自动机
    • 与构造 $DK$ 类似,我们先构造一个 NFA $K_1$,然后通过子集构造得到 DFA $DK_1$
    • $K_1$ 的状态: 就是上面定义的 LR(1) 项
    • $K_1$ 的转移:
    • 移进 (Shift): 如果当前项是 $[T \rightarrow u \cdot x v, a]$,当“读取”符号 $x$ 时,会转移到新项 $[T \rightarrow ux \cdot v, a]$。注意,前瞻符号 $a$ 在移进过程中保持不变
    • ε-转移 (Closure): 这是最关键的变化。如果当前有一个项 $[T \rightarrow u \cdot C v, a]$,其中点后面是一个变量 $C$。我们需要找到所有 $C$ 的产生式,比如 $C \rightarrow r$。对于每个这样的产生式,我们都要添加一个新的起始项 $[C \rightarrow \cdot r, b]$。这里的前瞻符号 $b$ 是如何确定的呢?
    • $b$ 是紧跟在 $C$ 后面的字符串 $v$ 所能推导出的所有可能开头的终结符(FIRST集)。
    • 特殊情况: 如果 $v$ 可以推导出空串 ε,那么 $C$ 规约完后,紧接着的符号就是原始项的前瞻符号 $a$。因此,这种情况下 $b$ 也可以是 $a$
    • 这个过程直观地体现了前瞻的传递
  3. $DK_1$ 检验的冲突定义
    • $DK_1$ 的状态LR(1) 项的集合
    • 检验的核心: 检查 $DK_1$ 的任何一个状态(即任何一个 LR(1) 项的集合),是否存在冲突。
    • 冲突的精确定义:
    • 移入/规约冲突: 状态中同时包含:
  4. 一个已完成的项(准备规约): $[A \rightarrow u \cdot, a]$
  5. 一个未完成的项(准备移入): $[B \rightarrow v \cdot b w, c]$,其中 $b$ 是一个终结符。

如果 $a = b$,就发生了冲突。因为当下一个输入符号是 $a$ 时,解析器不知道是应该根据第一条规则用前瞻 $a$规约,还是根据第二条规则移入符号 $a$

  • 规约/规约冲突: 状态中同时包含:
  1. 一个已完成的项: $[A \rightarrow u \cdot, a]$
  2. 另一个已完成的项: $[B \rightarrow v \cdot, b]$

如果 $a = b$,就发生了冲突。因为当下一个输入符号是 $a$ 时,解析器不知道是应该按 $A \rightarrow u$ 规约,还是按 $B \rightarrow v$ 规约。

  • 原文的“一致”定义: 原文用一个更紧凑的方式描述了冲突。一个状态里不能有两个“一致”的规则。
  • R1: [A -> u., a1]R2: [B -> v., a2] 是一致的,如果 a1=a2。 (规约/规约)
  • R1: [A -> u., a1]R2: [B -> v.aw, c] 是一致的,如果 a1=a。(移入/规约)
  • 如果一个状态中没有“一致”的带点规则,文法就通过 $DK_1$ 检验
💡 [数值示例]
  • 回顾 if-then-else 例子:
  • 文法:1. S' -> S, 2. S -> i S e S, 3. S -> i S, 4. S -> a (i=if, e=else, a=other_stmt)
  • 假设在解析过程中,我们到达一个 $DK_1$ 状态,它包含如下的项集合(简化表示):
  • { [S -> i S ·, e], [S -> i S · e S, $] }
  • 这里的 $ 是文件结束符。
  • 这个状态出现了移入/规约冲突吗?
  • 看第一项 [S -> i S ·, e],它是一个完成项(点在最后),它的前瞻是 e。这意味着,如果下一个输入符号是 e,我就可以规约
  • 看第二项 [S -> i S · e S, $],它是一个未完成项,点后面是终结符 e。这意味着,如果下一个输入符号是 e,我也可以移入
  • 因为前瞻 e 和移入符号 e 相同,所以这里存在一个移入/规约冲突。这个文法不是 LR(1) 的。
  • (注:经典的 if-then-else 歧义的解决方法是给 else 赋予更高的优先级,或者重写文法。这里的例子只是为了说明冲突如何被检测。)
  • 一个通过检验的例子:
  • 文法:$S \rightarrow aA \mid bB$, $A \rightarrow c$, $B \rightarrow c$
  • 初始状态会包含类似 { [S' -> .S, $], [S -> .aA, $], [S -> .bB, $] }
  • 假设移入 a 后,状态变为 { [S -> a.A, $], [A -> .c, c] }`。这里对 `A` 的前瞻是 `c` 吗?不对,`A` 后面没东西了,所以前瞻继承自 $S$,是 `$。所以状态是 { [S -> a.A, $], [A -> .c, $] }
  • 再移入 c,状态变为 { [A -> c., $] }`。这是一个完成项,前瞻是 `$。这个状态里没有其他项,所以没有冲突。可以安全地在遇到 $ 时规约。
⚠️ [易错点]
  1. 易错点: 计算 ε-转移 (Closure) 时的前瞻符号。这是最容易出错的地方。对于项 $[T \rightarrow u \cdot C v, a]$ 和规则 $C \rightarrow r$,新项 $[C \rightarrow \cdot r, b]$ 的前瞻 $b$ 来自 FIRST(v)。如果 $v$ 可以为空,则前瞻也包含 $a$。必须精确计算 FIRST 集合。
  2. 边界情况: 文法的开始规则。通常会增广文法,加入一条新规则 $S' \rightarrow S \dashv$,然后从 $[S' \rightarrow \cdot S \dashv, \text{EOF}]$ 开始,其中 EOF 是一个特殊的输入结束符号。这样可以保证初始的前瞻是明确的。原文例子中用了 a in Sigma,也是一种处理方式。
📝 [总结]

$DK_1$ 检验DK 检验的升级版,它通过在自动机的每个状态中增加“前瞻符号”的信息,来判断一个文法是否为 LR(1) 文法。其核心在于,将 LR(0) 检验中不可调和的冲突,通过检查前瞻符号来化解。只有当规约所需的前瞻符号和移入所需的下一个输入符号相同时,或者两个不同规约所需的前瞻符号相同时,才会判定为冲突。这使得 $DK_1$ 检验DK 检验更“宽容”,能够接受更多更自然的文法。

🎯 [存在目的]

本段的目的是为 LR(1) 文法提供一个形式化的、可操作的判定算法。有了 $DK_1$ 检验,我们就不再是凭感觉说一个文法“似乎是 LR(1) 的”,而是可以通过一个机械的、可由计算机执行的算法来精确地验证它。这是构建 LR(1) 解析器生成器(如 YACC, Bison 的理论基础)的第一步。

🧠 [直觉心智模型]
  1. DK 检验: 一个严格的考官,他只看你卷面上写下的答案,如果一个地方看起来模棱两可,就直接判错。
  2. $DK_1$ 检验: 一个更通情达理的考官。当他看到一个模棱两可的答案时,他会说:“别急,让我看看你的下一道题是怎么答的(前瞻)。” 如果你下一题的答案能消除当前答案的歧义,他就会判你对。例如,你在一道填空题里写了 state,他不知道你指的是“州”还是“状态”。但他看到你下一题的答案是“加利福尼亚”,他就知道你这里指的是“州”,于是就不判错了。
💭 [直观想象]

想象你在一个岔路口。

  1. LR(0) 决策: 你只看到脚下的路标写着“目的地 A”和“目的地 B”都从这里分岔。你不知道该走哪条,于是你“冲突”了,停在原地。
  2. LR(1) 决策: 你不仅看脚下的路标,你还抬头看了看远处(前瞻)。
  3. 你看到左边那条路 1 公里外有个牌子写着“欢迎来到 A 城”。
  4. 你看到右边那条路 1 公里外有个牌子写着“B 城加油站”。
  5. 如果你的目标是 A 城,你就毫不犹豫地选择左边。
  6. $DK_1$ 检验就是系统地检查这个文法的所有“岔路口”,确保在每个路口,只要你允许“抬头看一公里远”,就总能做出唯一的、正确的选择。

3.4 定理 2.63

📜 [原文15]

$G$ 通过 $DK_1$ 检验当且仅当 $G$LR(1) 文法

📖 [逐步解释]

这是一个定义性的定理,它将一个抽象的数学定义(LR(1) 文法)与一个具体的、可执行的算法($DK_1$ 检验)等同起来。

  1. 定理内容: "一个文法 $G$LR(1) 文法" 这个陈述,与 "文法 $G$ 通过 $DK_1$ 检验" 这个陈述,是完全等价的。
    • LR(1) 文法 (定义): 在任何有效字符串中,句柄都是由前瞻 1 强制的。这是一个比较抽象的、关于整个语言和所有推导的性质的描述。
    • $DK_1$ 检验 (算法): 一个构造性的、机械的检查过程,通过构建 $DK_1$ 自动机并检查其状态中是否存在特定类型的冲突。
  2. 定理的意义: 这个定理为我们提供了一个“抓手”。我们不再需要去遍历一个文法生成的所有无限多个有效字符串来检查“句柄是否强制”,只需要运行一次 $DK_1$ 检验算法即可。如果算法报告没有冲突,我们就能百分之百确定这个文法是 LR(1) 的。
  3. 证明思路部分:
    • 原文中 "推论 2.51 仍然适用于 $DK_1$,因为我们可以忽略前瞻符号" 这句话给出了证明的一个提示。
    • 推论 2.51 (在本书前面部分) 大概是说 $DK$ 自动机能够正确地识别文法的所有有效前缀。
    • 这句话的意思是,$DK_1$ 自动机本质上就是 $DK$ 自动机加上了前瞻信息。如果我们把所有的前瞻符号都擦掉,剩下的结构就和 $DK$ 完全一样,所以它同样能识别有效前缀。
    • 完整的证明会展示,$DK_1$ 状态中的冲突,精确地对应于文法中存在某个句柄在某个前瞻符号下不是“强制”的情况。反之亦然。
📝 [总结]

定理 2.63LR(1) 理论的基石。它声明,判断一个文法是否为 LR(1) 的抽象定义,可以完全等价地用一个具体的 $DK_1$ 检验算法来判定。这使得 LR(1) 文法的识别问题从一个理论问题转化为了一个工程问题。

🎯 [存在目的]

本定理的目的是将 LR(1) 文法的理论定义与其实际应用连接起来。在它之前,LR(1) 只是一个数学概念;在它之后,LR(1) 成为了一个可以通过算法来验证的属性。这是所有 LR(1) 解析器生成工具(如 YACC, Bison)能够存在的理论前提。这些工具的核心功能之一,就是实现了 $DK_1$ 检验(或其变体),以判断用户输入的文法是否符合要求。

🧠 [直觉心智模型]
  1. LR(1) 文法定义: 就像是法律条文里对“好公民”的抽象定义:“一个好公民是在任何情况下,考虑到后续一种可能发生的后果后,其行为总是唯一且符合道德的。”
  2. $DK_1$ 检验: 就像是一套具体的“好公民资格考试”。考卷上列出了各种情景($DK_1$ 状态),并问你在这种情景下,如果接下来可能发生某事(前瞻),你会怎么做。
  3. 定理 2.63: 就是在说:“这套资格考试是完美设计的,一个人能通过考试,当且仅当他是一个真正的‘好公民’。” 它将抽象的品质衡量转化为了标准化的测试。
💭 [直观想象]

想象医生诊断一种罕见的过敏症。

  1. LR(1) 文法定义 (疾病的理论定义): “花生过敏症”是指病人的免疫系统在接触花生蛋白后,考虑到后续1小时内可能出现的呼吸道反应,会产生一种唯一的、剧烈的免疫应答。
  2. $DK_1$ 检验 (诊断方法): 医生发明了一种新的皮试技术。他将微量花生蛋白提取物,以及一种模拟后续呼吸道反应的化学指示剂(前瞻),同时注射到病人皮下。然后观察特定区域是否出现红肿(冲突)。
  3. 定理 2.63 (诊断方法的有效性证明): 一篇顶级医学论文证明了,这种新的皮试技术出现阳性反应(测试不通过),当且仅当病人确实患有理论上定义的那种“花生过敏症”。这个定理使得诊断从一个需要长期观察和推断的复杂过程,变成了一个标准、可靠、快速的测试。

3.5 示例 2.64

📜 [原文16]

这个示例表明以下文法通过了 $DK_1$ 检验。回想一下,在示例 2.53 中,这个文法未能通过 DK 检验。因此,它是一个 LR(1) 文法但不是 DCFG 的例子。

$$ \begin{aligned} & S \rightarrow E-1 \\ & E \rightarrow E+T \mid T \\ & T \rightarrow T \times \text{a} \mid \text{a} \end{aligned} $$

图 2.65

通过 $DK_1$ 检验

📖 [逐步解释]

本段通过一个具体的例子,生动地展示了 LR(1) 相对于 LR(0) (DCFG) 的优势。

  1. 例子的目的: 找出一个文法,它不是 DCFG (即通不过 DK 检验),但却是 LR(1) 文法 (即能通过 $DK_1$ 检验)。这个例子是证明 LR(1)DCFG 更“宽容”、更具表达力的直接证据。
  2. 文法分析:
    • 这是一个典型的算术表达式文法,但做了一些简化。
    • S -> E-1: 整个表达式必须以 -1 结尾。
    • E -> E+T | T: 表达式 E 是由 T 通过 + 连接而成。
    • T -> Ta | a: 项 T 是由 a 通过 连接而成。
    • 这个文法定义了 +* 优先级低,并且两者都是左结合的。
  3. 为什么它通不过 DK 检验 (不是 LR(0))?
    • 让我们模拟一下 DK 自动机的构造。
    • 考虑解析器已经读入了一个 a。这个 a 可以被规约为 T (根据 $T \rightarrow a$)。
    • 此时,一个 DK 状态中会包含类似 { T -> a. } 这样的完成项。
    • 但是,这个 T 接下来可以被 * 跟随(根据 $T \rightarrow T \cdot * a$),也可以被 + 跟随(因为 T 可以被规约为 E,然后根据 $E \rightarrow E \cdot + T$)。
    • 问题出在规约 TE。当 T 被看作 E 时 (通过规则 $E \rightarrow T$),解析器会面临一个冲突。
    • 假设解析器看到了一个 T。它需要决定:
    • 规约?: 将 T 规约为 E (根据 $E \rightarrow T$)。
    • 移入?: 如果下一个符号是 ,则应该移入 ,以匹配规则 $T \rightarrow T * a$
    • 在一个 LR(0) 解析器中,当它看到 T 时,它的状态中既有完成项 E -> T.,又有未完成项 T -> T . * a。这是一个移入/规约冲突。解析器不知道该怎么做。因此,这个文法不是 LR(0) (DCFG)。
  4. 为什么它能通过 $DK_1$ 检验 (是 LR(1))?
    • 现在我们用 LR(1) 的视角来看。当解析器看到了 T,面临同样的决策时,它会向前看一个符号
    • 情况A: 如果下一个符号是 *
    • E \rightarrow T 这个规约动作,其合法的前瞻符是什么?E 后面可以跟 + 或者 -1。所以 * 不在 E 的前瞻集里。
    • 因此,当下一个符号是 时,唯一的合法动作就是移入
    • 情况B: 如果下一个符号是 +
    • +E 的合法前瞻符。
    • T 后面可以跟 *,但不能跟 +
    • 所以当下一个符号是 + 时,唯一的合法动作就是规约 TE
    • 情况C: 如果下一个符号是 - (来自 -1)。
    • - 也是 E 的合法前瞻符。
    • 唯一的合法动作也是规约 TE
    • 结论: 通过检查下一个符号,解析器总能做出唯一的决定。
    • 看到 * -> 移入
    • 看到 +- -> 规约
    • 由于不存在任何一个前瞻符号同时允许移入和规约,所以没有冲突。文法通过 $DK_1$ 检验,是 LR(1) 的。
  5. 关于图 2.65:
    • 图中展示了为该文法构造出的 $DK_1$ 自动机的一部分。
    • 每个方框是一个 $DK_1$ 的状态,里面是 LR(1) 项的集合。
    • 例如,在状态 I2 中,有项 [T -> a., +][T -> a., ]。这表示在读了 a 之后,如果下一个符号是 +,都应该将 a 规约为 T
    • 在状态 I4 (E+T) 中,有 [E -> E+T., +][T -> T., *]
    • 这是一个潜在的冲突点。我们有一个完成的 E -> E+T. 和一个完成的 T -> T.(通过 $E->T$ 来的)。
    • E -> E+T. 的前瞻是 + (和 -)。
    • T -> T.a 的移入是
    • 因为 {+, -}{*} 不相交,所以没有冲突。
    • 图示的目的就是直观地展示在任何一个状态里,对于任何一个前瞻符号,动作都是唯一的。
∑ [公式拆解]

$$ \begin{aligned} & S \rightarrow E-1 \\ & E \rightarrow E+T \mid T \\ & T \rightarrow T \times \text{a} \mid \text{a} \end{aligned} $$

  • $S, E, T$: 变量 (Non-terminals)。分别代表“语句”、“表达式”、“项”。
  • $-1, +, \times, \text{a}$: 终结符 (Terminals)。代表语言中的实际符号。a 可以看作是数字或标识符。
  • $S \rightarrow E-1$: 句法上,一个合法的程序是一个表达式 E 后面跟着 -1
  • $E \rightarrow E+T \mid T$: 这是一个左递归的规则。它定义了 + 的左结合性。一个 E 可以是另一个 E 加上一个 T,或者直接就是一个 T
  • $T \rightarrow T \times \text{a} \mid \text{a}$: 也是左递归,定义了 * 的左结合性。一个 T 可以是另一个 T 乘以 a,或者直接就是一个 a
  • 优先级: 因为 E (包含 +) 是由 T 构成的,而 T (包含 ) 是由 a 构成的,这暗示了 的运算优先级高于 +。例如,a+aa 会被解析为 a + (aa)
⚠️ [易错点]
  1. 易错点: 在手动推演 $DK_1$ 状态时,最容易出错的是计算前瞻集 (lookahead set)。例如,对于 E -> E+T.,它的前瞻集是 FOLLOW(E),即所有可能跟在 E 后面的终结符。在这个文法里,E 后面只能跟 +(来自 E->E+T)和 -(来自 S->E-1)。精确计算 FOLLOW 集是正确判断冲突的关键。
📝 [总结]

本示例通过一个具体的算术表达式文法,成功地展示了 LR(1) 的威力。这个文法因为存在“移入/规约冲突”而无法被 LR(0) (DCFG) 解析,但通过引入一个符号的前瞻,这些冲突都得到了解决。这直观地证明了 LR(1) 文法的集合确实是真包含 DCFG 集合的,并且 LR(1) 在描述常见编程语言结构(如带优先级的运算符)方面更加自然和强大。

🎯 [存在目的]

本示例的目的是提供一个具体的、有说服力的证据,来支撑“LR(1) 比 DCFG 更方便”这个论点。理论的阐述往往是抽象的,一个好的例子能让读者瞬间抓住核心差异。这个例子就是 LR(0)LR(1) 分水岭上的一个界碑,它让读者亲眼看到“前瞻”是如何实实在在地解决问题的。

🧠 [直觉心智模型]
  1. LR(0) 解析器面对这个文法: 就像一个只懂“先乘除后加减”但非常死板的计算器。当它看到 a+a 时,它会想:“a 可以是一个 Ta 也可以是一个 E,我到底该把它看成什么?” 它陷入了混乱。
  2. LR(1) 解析器: 就像一个更聪明的计算器。当它看到 a+a 时,它会向后偷看一下,如果看到后面是 *,它就知道“哦,这个 a 后面要参与乘法,它必须被看作一个 T”。如果看到后面是 -1,它就知道“哦,加法是最后一级运算了,可以把 a+a 看作一个完整的 E”。
💭 [直观想象]

想象你在一条流水线上组装产品。

  1. 文法: 组装图纸。
  2. 冲突: 图纸上有一步说“安装A零件”,但A零件有两种:A1(用于后续步骤需要安装B零件的场合)和A2(用于后续步骤需要安装C零件的场合)。
  3. LR(0) 工人: 他只看到当前步骤“安装A零件”,他不知道该用A1还是A2,于是停工了(冲突)。
  4. LR(1) 工人: 他不仅看当前步骤,还会看传送带上下一个要被送来的零件是什么(前瞻)。如果他看到下一个是B零件,他就拿A1来装;如果看到是C零件,他就拿A2来装。他总能做出正确的选择,流水线可以顺畅运行。
  5. 这个文法对于 LR(0) 工人来说是有问题的,但对于 LR(1) 工人来说是完全可行的。

3.6 定理 2.66

📜 [原文17]

一个带结束标记的语言可由 LR(1) 文法生成当且仅当它是一个 DCFL

我们已经证明每个 DCFL 都有一个 LR(0) 文法,因为 LR(0) 文法DCFG 相同。这证明了定理的反向。剩下的是以下引理,它展示了如何将 LR(1) 文法转换为 DPDA

📖 [逐步解释]

这个定理是 定理 2.57 的一个平行版本,只是把 DCFG 替换成了更强大的 LR(1) 文法

  1. 定理陈述: "一个带结束标记的语言可由 LR(1) 文法生成" 和 "它是一个 DCFL" 这两件事是等价的。
    • 这再次在生成模型(这次是 LR(1) 文法)和识别模型DPDA,其语言即 DCFL)之间划上了等号。
    • 前提条件依然是 "带结束标记的语言"。
  2. 证明策略: 这是一个“当且仅当”的定理,需要双向证明。
    • 反向证明 (⇐): 如果一个语言是带结束标记的 DCFL,那么它可以由 LR(1) 文法生成。
    • 作者指出,这一方向的证明已经间接完成了。
    • 逻辑链是:我们之前在 引理 2.59 中证明了,任何一个带结束标记的 DCFL 都有一个等价的 DCFG
    • DCFG 就是 LR(0) 文法
    • 任何一个 LR(0) 文法自然也是一个 LR(1) 文法(因为 LR(1) 的要求比 LR(0) 更宽松)。
    • 所以,任何一个带结束标记的 DCFL 都有一个等价的 LR(1) 文法。反向得证。
    • 正向证明 (⇒): 如果一个语言可由 LR(1) 文法生成,那么它是一个 DCFL
    • 要证明这一点,我们需要展示如何将任何一个 LR(1) 文法转换为一个等价的 DPDA (因为 DPDA 识别的正是 DCFL)。
    • 作者明确指出,这个任务将由接下来的引理 2.67 来完成。
📝 [总结]

定理 2.66 最终确立了 LR(1) 文法DCFL 之间的等价关系(在带结束标记的前提下)。它通过引用之前的结论(DCFL → LR(0))轻松地证明了其中一个方向,并将另一个方向的证明任务(LR(1) → DPDA)交给了下一个引理。这个定理是本章关于确定性语言理论的一个重要总结。

🎯 [存在目的]

本定理的目的是将前面引入的 LR(1) 概念,正式地、最终地纳入到 DCFL 的理论框架中。它明确宣告,尽管 LR(1) 文法看起来比 DCFG 更强大,但它们并没有突破 DCFL 的边界。这为在实践中使用 LR(1) 文法来设计和解析所有 DCFL 提供了坚实的理论依据。

🧠 [直觉心智模型]

这就像在证明“所有可以用 Python 写的程序,都是图灵可计算的”。

  1. 反向证明: 我们知道所有图灵可计算的问题,都能用最基础的图灵机模型来解决(相当于 LR(0))。而 Python 至少和图灵机一样强大。所以所有图灵可计算的问题都能用 Python 解决。
  2. 正向证明: 我们需要证明,Python 里的所有语法结构(循环、函数调用等),最终都能被翻译成图灵机的一系列基本操作(相当于 DPDA)。这个证明比较复杂(对应引理 2.67)。
  3. 定理 2.66: 最终结论就是,Python 的表达能力和图灵机的计算能力是等价的。
💭 [直观想象]

想象两种不同级别的建筑蓝图。

  1. DCFL: 所有可以被一个确定性机器人质检员盖章合格的建筑。
  2. LR(0) 蓝图 (DCFG): 一种非常严格、无任何歧义、但画起来很费劲的蓝图。
  3. LR(1) 蓝图: 一种更先进的蓝图,允许使用一些“智能注释”,比如“此处墙壁的厚度取决于隔壁房间是否是承重墙(前瞻)”。
  4. 定理 2.66 在说:
  5. 反向: 任何一座能被机器人质检员认可的建筑,我们总能为它画出一张(可能很复杂的)LR(0) 蓝图。而 LR(0) 蓝图本身就是一种 LR(1) 蓝图。所以,合格建筑总有对应的 LR(1) 蓝图。
  6. 正向: 任何一张 LR(1) 蓝图所描述的建筑,最终也一定能被那个确定性的机器人质检员所识别和接受。这个需要下一个引理来证明。

3.7 引理 2.67

📜 [原文18]

每个 LR(1) 文法都有一个等价的 DPDA

📖 [逐步解释]

这个引理是 定理 2.66 的最后一块拼图,也是 LR(1) 理论能够实际应用的关键。

  1. 引理的核心: 任何一个 LR(1) 文法,我们总能构造出一个等价的确定性下推自动机 (DPDA)
  2. 等价性: $L(G) = L(P)$,其中 $G$ 是给定的 LR(1) 文法,$P$ 是构造出的 DPDA
  3. 意义:
    • 它完成了 定理 2.66 的正向证明。
    • 它表明,LR(1) 文法生成的所有语言都在 DPDA 的识别能力范围之内,即它们都是 DCFL
    • 最重要的是,这个证明是构造性的,它提供了一个从 LR(1) 文法到 DPDA 的算法。这个算法正是 LR(1) 解析器的工作原理。
📝 [总结]

引理 2.67 声明了一个从 LR(1) 文法DPDA 的构造性路径的存在。这保证了任何用 LR(1) 文法描述的语言,都可以被一个高效的、确定性的解析器所识别。

🎯 [存在目的]

本引理的目的是为 定理 2.66 提供最后的支持,并从根本上阐明 LR(1) 解析器是如何工作的。它的证明思路(如下一段所述)实际上就是 LR(1) 解析算法的理论描述。

🧠 [直觉心智模型]

引理说:“只要你给我一本用‘高级语言’(LR(1))写的、无歧义的说明书,我就能造一个机器人(DPDA),这个机器人能严格按照这本说明书来验证产品。”

💭 [直观想象]

回到那个会“抬头看路”的司机(LR(1) 解析器)的比喻。

  1. LR(1) 文法: 一份带有“前瞻提示”的完美驾驶指南。
  2. 引理 2.67: 保证了我们一定可以根据这份指南,训练出一个真正的司机(DPDA),他有大脑(有限状态控制)、有记忆(栈),并且能“抬头看一个路标远”(前瞻能力),从而能够沿着任何一条符合指南的路线,确定地、无差错地从起点开到终点。

37.1 证明思路

📜 [原文19]

我们构造 $P_1$,它是我们在引理 2.67 中介绍的 DPDA $P$ 的修改版本。 $P_1$ 读取其输入并模拟 $DK_1$,同时使用栈来跟踪 $DK_1$ 如果所有规约步骤都应用到目前为止的输入,它将处于什么状态。此外,$P_1$ 超前读取 1 个符号,并将此前瞻信息存储在其有限状态存储器中。每当 $DK_1$ 达到接受状态时,$P_1$ 都会查阅其前瞻以查看是否执行规约步骤,以及如果在此状态中出现多种可能性,执行哪个步骤。只能应用一个选项,因为文法LR(1)

📖 [逐步解释]

这部分描述了如何构造一个能识别 LR(1) 文法的 DPDA,即 LR(1) 解析算法的原理。

  1. 基础:引理 2.58 的 DPDA P
    • 证明的起点是我们在将 DCFG (LR(0)) 转换为 DPDA 时所用的那个 DPDA $P$
    • 那个 DPDA $P$ 的核心思想是:在栈里存储 DK 自动机的状态,来模拟一个移入-规约过程。
  2. 为 LR(1) 进行升级:构造 DPDA $P_1$
    • 新的 DPDA $P_1$ 同样在栈里存储状态,但它模拟的是 $DK_1$ 自动机,而不是 $DK$
    • 核心增强: $P_1$ 被赋予了前瞻 (lookahead) 的能力。
    • $P_1$ 的“有限状态存储器” (即它的状态集合) 被设计得更复杂,用来额外存储一个终结符
    • 在任何时候,$P_1$ 不仅知道自己当前的状态,还知道输入流中的下一个符号是什么。它总是“超前读取 1 个符号”。
  3. $P_1$ 的工作流程 (LR(1) 解析算法)
    • 移入 (Shift): 和以前一样,$P_1$ 读取输入,模拟 $DK_1$ 的状态转移,并将新状态压栈。同时,它更新自己存储的“下一个输入符号”。
    • 决策点 (规约或移入?): 当 $P_1$ 模拟的 $DK_1$ 到达一个包含完成项的状态时,它就面临一个决策。
    • 这个 $DK_1$ 状态可能包含一个或多个完成项(如 [A -> u., a]),也可能包含未完成项(如 [B -> v.bw, c])。
    • 此时,$P_1$ 拿出它预先存储的前瞻符号(即当前输入流中真正的下一个符号)。
    • 它用这个前瞻符号去匹配当前 $DK_1$ 状态里的所有可能性:
    • 如果前瞻符号匹配了某个完成项 [A -> u., a] 的前瞻 a,那么规约就是一个合法的选项。
    • 如果前瞻符号匹配了某个未完成项 [B -> v.bw, c] 的移入符号 b,那么移入就是一个合法的选项。
    • 利用 LR(1) 性质:
    • 因为我们假设文法是 LR(1) 的,所以它通过了 $DK_1$ 检验
    • 这意味着,对于任何一个前瞻符号,在任何一个 $DK_1$ 状态中,合法的选项永远只有一个
    • 不可能会出现“既可以规约又可以移入”或“可以按两种不同方式规约”的情况。
    • 执行: $P_1$ 根据这个唯一的合法选项,执行相应的规约移入操作。
    • 整个过程因此是确定性的。
💡 [数值示例]
  • 回顾 a+a*a 的例子:
  • 文法是 LR(1) 的: $E \rightarrow E+T \mid T$, $T \rightarrow T*a \mid a$ (简化)
  • 假设 DPDA $P_1$ 已经解析了 a,并将其规约为了 T。现在栈顶代表的状态(比如 $I_x$)包含了冲突的可能:
  • E -> T. (规约)
  • T -> T.*a (移入)
  • $P_1$ 此时已经超前读取了下一个符号。
  • 情况A: 下一个符号是 *
  • $P_1$ 检查其“前瞻存储器”,发现是 *
  • 它查看状态 $I_x$E -> T. 的前瞻集,发现是 {+, $}* 不在其中。所以规约不是选项。
  • 它查看状态 $I_x$T -> T.a,发现移入符号是 。匹配成功。
  • 唯一的动作是移入 *
  • 情况B: 下一个符号是 +
  • $P_1$ 的前瞻存储器里是 +
  • E -> T. 的前瞻集包含 +。规约是合法选项。
  • T -> T.a 的移入符号是 。不匹配。
  • 唯一的动作是规约 TE
  • 通过这种方式,$P_1$ 总能确定地执行下去。
⚠️ [易错点]
  1. 易错点: 如何实现“超前读取1个符号”。在真实的 DPDA 模型中,这通常是通过将状态设计为一个二元组 (q, a) 来实现的,其中 q 是核心状态,a 是缓存的前瞻符号。DPDA 的转移函数会同时消耗当前符号和更新缓存的下一个符号。
  2. 边界情况: 输入结束时。当超前读取的符号是文件结束符(EOF\dashv)时,DPDA 用它来进行最后的决策。
📝 [总结]

该证明思路清晰地描述了 LR(1) 解析器的核心算法。它通过对 LR(0) 解析器 (DPDA $P$) 进行两项关键升级:1) 模拟更强大的 $DK_1$ 自动机2) 增加缓存一位的前瞻能力,从而构建出一个新的 DPDA $P_1$。这个 $P_1$ 在遇到决策点时,能够利用缓存的前瞻符号和 $DK_1$ 状态中包含的前瞻信息,来唯一地、确定地选择下一步动作(移入或规约)。因为文法的 LR(1) 性质保证了这种选择的唯一性,所以 $P_1$ 是一个合法的 DPDA

🎯 [存在目的]

本段的目的在于揭示理论如何转化为算法。它不仅是引理 2.67 的证明梗概,更是对所有 LR(1) 解析器工作原理的精辟总结。它告诉我们,LR(1) 文法的“前瞻”特性,不是一个虚无缥缈的数学性质,而是可以被一个确定性机器(DPDA)通过“多看一个符号”的机制来具体实现的。

🧠 [直觉心智模型]

这就像一个升级版的流水线工人(DPDA $P_1$)。

  1. 普通工人 (DPDA P): 只看手里的零件,根据图纸决定是拧螺丝还是焊接。
  2. 高级工人 (DPDA $P_1$): 他的工作台上有一个小小的“暂存区”,总是放着传送带上下一个要送来的零件(前瞻)。
  3. 当他手上的组件可以有多种处理方式时(决策点),他会瞟一眼暂存区的那个零件。
  4. “哦,下一个是圆形零件,那我现在手上的这个必须拧上一个六角螺母(规约)。”
  5. “哦,下一个是方形零件,那我手上的组件还不能完成,必须再焊上一个支架(移入)。”
  6. 因为图纸(LR(1) 文法)是精心设计的,所以无论暂存区里出现什么零件,高级工人的操作选择永远是唯一的。
💭 [直观想象]

想象你在参加一个电视智力竞赛,需要快速回答问题。

  1. : DPDA $P_1$
  2. 你的大脑: 有限状态控制器。
  3. 你的短期记忆: 栈。
  4. 你戴的特殊眼镜: 你的“前瞻”能力。这副眼镜能让你看到主持人手卡上即将要念出的下一个单词。
  5. 答题过程:
  6. 主持人开始念题:“第一个发现美洲的欧洲人是...”
  7. 当你听到“欧洲人”时,你的大脑里可能有两个答案在打架:“哥伦布”或“维京人”。这是一个冲突点。
  8. 但你通过特殊眼镜,看到了主持人手卡上的下一个词是“...并建立了持久的殖民地...”。
  9. 这个前瞻信息让你立即排除了“维京人”(因为他们没有建立持久殖民地)。
  10. 你毫不犹豫地、确定地按下了抢答器,并回答“哥伦布”。
  11. LR(1) 文法就保证了,对于任何问题,你眼镜里看到的下一个词,都足以让你在所有可能的答案中做出唯一的正确选择。

44. 练习

📜 [原文20]

2.1 回想一下我们在示例 2.4 中给出的 CFG $G_4$。为方便起见,我们将其变量重命名为单个字母,如下所示。

$$ \begin{aligned} & E \rightarrow E+T \mid T \\ & T \rightarrow T \times F \mid F \\ & F \rightarrow(E) \mid \text{a} \end{aligned} $$

给出每个字符串的分析树推导

a. a

b. a+a

c. a+a+a

d. ((a))

2.2 a. 使用语言 $A=\left\{\mathrm{a}^{m} \mathrm{~b}^{n} \mathrm{c}^{n} \mid m, n \geq 0\right\}$$B=\left\{\mathrm{a}^{n} \mathrm{~b}^{n} \mathrm{c}^{m} \mid m, n \geq 0\right\}$ 以及示例 2.36 来证明上下文无关语言类在交集下不封闭。

b. 使用 (a) 部分和德摩根定律 (定理 0.20) 来证明上下文无关语言类在补运算下不封闭。

${ }^{\text{A}}$ 2.3 对以下上下文无关文法 $G$ 的每个部分回答。

$$ \begin{aligned} R & \rightarrow X R X \mid S \\ S & \rightarrow \mathrm{a} T \mathrm{~b} \mid \mathrm{b} T \mathrm{a} \\ T & \rightarrow X T X|X| \varepsilon \\ X & \rightarrow \mathrm{a} \mid \mathrm{b} \end{aligned} $$

a. $G$变量是什么?

i. 对或错:$T \stackrel{*}{\Rightarrow} T$

b. $G$终结符是什么?

j. 对或错:$XXX \xrightarrow{*}$ aba。

c. $G$开始变量是哪个?

k. 对或错:$X \stackrel{*}{\Rightarrow}$ aba。

d. 给出 $L(G)$ 中的三个字符串。

l. 对或错:$T \stackrel{*}{\Rightarrow} XX$

e. 给出不在 $L(G)$ 中的三个字符串。

m. 对或错:$T \stackrel{*}{\Rightarrow} XXX$

f. 对或错:$T \Rightarrow$ aba。

n. 对或错:$S \stackrel{*}{\Rightarrow} \varepsilon$

g. 对或错:$T \stackrel{*}{\Rightarrow}$ aba。

o. 用英语描述 $L(G)$

h. 对或错:$T \Rightarrow T$

2.4 给出生成以下语言的上下文无关文法。在所有部分中,字母表 $\Sigma$$\{0,1\}$

${ }^{\text{A}}$ a. $\{w \mid w \text{ 包含至少三个 } 1 \text{s}\}$

b. $\{w \mid w \text{ 以相同符号开始和结束}\}$

c. $\{w \mid w \text{ 的长度为奇数}\}$

${ }^{\text{A}}$ d. $\{w \mid w \text{ 的长度为奇数且其中间符号为 } 0\}$

e. $\{w \mid w=w^{\mathcal{R}} \text{,即 } w \text{ 是回文}\}$

f. 空集

2.5 给出练习 2.4 中语言的下推自动机的非形式化描述和状态图。

2.6 给出生成以下语言的上下文无关文法

${ }^{\text{A}}$ a. 字母表 $\{\mathrm{a}, \mathrm{b}\}$ 上 a 比 b 多的字符串集

b. 语言 $\left\{\mathrm{a}^{n} \mathrm{~b}^{n} \mid n \geq 0\right\}$补集

${ }^{\text{A}}$ c. $\left\{w \# x \mid w^{\mathcal{R}} \text{ 是 } x \text{ 的子字符串,其中 } w, x \in\{0,1\}^{*}\right\}$

d. $\left\{x_{1} \# x_{2} \# \cdots \# x_{k} \mid k \geq 1\text{,每个 } x_{i} \in\{\mathrm{a}, \mathrm{b}\}^{*}\text{,并且对于某个 } i \text{ 和 } j\text{,} x_{i}=x_{j}^{\mathcal{R}}\right\}$

${ }^{\text{A}}$ 2.7 给出练习 2.6 中语言的 PDA 的非形式化英语描述。

${ }^{\text{A}}$ 2.8 证明字符串 the girl touches the boy with the flower 在第 103 页的文法 $G_2$ 中有两个不同的最左推导。用英语描述这个句子的两种不同含义。

2.9 给出生成语言

$$ A=\left\{\mathrm{a}^{i} \mathrm{~b}^{j} \mathrm{c}^{k} \mid i=j \text{ 或 } j=k \text{ 其中 } i, j, k \geq 0\right\} . $$

上下文无关文法。您的文法是否有歧义?为什么或为什么不?

2.10 给出识别练习 2.9 中语言 $A$下推自动机的非形式化描述。

2.11 使用定理 2.20 中给出的过程,将练习 2.1 中给出的 CFG $G_4$ 转换为等价的 PDA

2.12 使用定理 2.20 中给出的过程,将练习 2.3 中给出的 CFG $G$ 转换为等价的 PDA

2.13 令 $G=(V, \Sigma, R, S)$ 为以下文法$V=\{S, T, U\};\Sigma=\{0, \#\}$$R$ 是规则集:

$$ \begin{aligned} & S \rightarrow T T \mid U \\ & T \rightarrow 0 T|T 0| \# \\ & U \rightarrow 0 U 00 \mid \# \end{aligned} $$

a. 用英语描述 $L(G)$

b. 证明 $L(G)$ 不是正则的。

2.14 使用定理 2.9 中给出的过程,将以下 CFG 转换为乔姆斯基范式中的等价 CFG

$$ \begin{aligned} & A \rightarrow B A B|B| \varepsilon \\ & B \rightarrow 00 \mid \varepsilon \end{aligned} $$

2.15 给出反例,以证明以下构造未能证明上下文无关语言类在星运算下是闭合的。设 $A$ 是由 CFG $G=(V, \Sigma, R, S)$ 生成的 CFL。添加新规则 $S \rightarrow SS$,并将所得文法命名为 $G'$。这个文法应该生成 $A^*$

2.16 证明上下文无关语言类在正则运算并集连接星运算下是闭合的。

2.17 使用练习 2.16 的结果,给出另一个证明,证明每个正则语言都是上下文无关的,通过展示如何将正则表达式直接转换为等价的上下文无关文法

📖 [逐步解释]

这一部分是教材第二章的课后练习题,旨在巩固本章学习的核心概念,包括上下文无关文法 (CFG)下推自动机 (PDA)乔姆斯基范式 (CNF)确定性与非确定性歧义性以及闭包性质等。

  • 练习 2.1: 核心考察对推导 (derivation)分析树 (parse tree) 的理解。这是一个基础练习,要求学生能够手动模拟文法的生成过程,并将其可视化。
  • 练习 2.2: 考察 CFL闭包性质。这是一个经典的证明题,利用反例来证明 CFL交集补集运算下是不封闭的。这揭示了 CFL 相对于正则语言的一个重要弱点。
  • 练习 2.3: 考察对文法基本定义和推导关系 (一步推导) vs *⇒ (零步或多步推导) 的掌握。要求学生能识别文法的组成部分,并判断各种推导关系是否成立,最终理解文法所描述的语言。
  • 练习 2.4: 考察设计 CFG 的能力。要求学生为一系列具有特定模式的语言构造生成文法。这是从“分析文法”到“创造文法”的进阶。
  • 练习 2.5: 考察设计 PDA 的能力,与 2.4 配对。要求为相同的语言设计识别它们的下推自动机,从而在 CFGPDA 之间建立直观的联系。
  • 练习 2.6 & 2.7: 难度更高的 CFGPDA 设计题,涉及更复杂的语言结构,如数量比较、子字符串关系和回文等。
  • 练习 2.8: 考察歧义性 (ambiguity)。要求学生找到一个句子的两种不同推导,并解释这两种推导在自然语言理解中对应的不同含义,从而揭示语法歧义的实际影响。
  • 练习 2.9 & 2.10: 再次结合 CFGPDA,处理一个由“或”连接的复合语言,并探讨其歧义性。
  • 练习 2.11 & 2.12: 考察对 CFG → PDA 构造算法(定理 2.20)的掌握。要求学生机械地、按步骤地执行算法,将给定的文法转化为等价的 PDA
  • 练习 2.13: 综合性题目,要求理解一个较复杂的文法生成的语言,并证明该语言不是正则的(通常使用泵引理)。
  • 练习 2.14: 考察对 CFG → CNF 转换算法的掌握。乔姆斯基范式是一种重要的文法范式,在某些算法(如 CYK 解析算法)中是必需的。
  • 练习 2.15: 考察批判性思维,要求学生为一种错误的“证明”构造一个反例。这训练学生不仅仅是接受定理,还要能识别证明中的漏洞。
  • 练习 2.16 & 2.17: 考察 CFL 的闭包性质(在并集、连接、星号下是封闭的),并利用这些性质来提供一个从正则表达式CFG 的直接构造,从而再次证明正则语言CFL 的子集。
📝 [总结]

练习部分是理论学习的延伸和巩固,覆盖了从基本概念理解、文法与自动机设计、算法应用到理论性质证明的方方面面。通过完成这些练习,学生可以将抽象的理论知识转化为解决具体问题的技能。

🎯 [存在目的]

练习题的存在是为了让学生主动地与所学知识进行互动,通过实践来加深理解、发现盲点、并锻炼解决问题的能力。它们是连接“听懂了”和“会用了”之间的桥梁。

🧠 [直觉心智模型]

学习理论就像是学习一本武功秘笈。

  1. 看书: 理解了每一招每一式的理论。
  2. 练习: 找一个木人桩或者和师兄弟对练。
  3. 练习 2.1 是“打固定靶”,练习基本招式。
  4. 练习 2.4, 2.5 是“命题作文”,根据要求组合招式。
  5. 练习 2.2, 2.16 是“内功心法”,理解这门武功的边界和特性。
  6. 练习 2.11, 2.14 是“按图索骥”,严格按照秘笈上的某个复杂图谱来修炼。
  7. 练习 2.15 是“辨假”,识别出那些看似正确但实则错误的“伪秘笈”。
💭 [直观想象]

学习本章理论就像是学习了如何读懂和设计电路图(CFG)以及如何构建和理解实际的电路(PDA)。练习题就像是:

  1. 给你一张电路图,让你分析它的功能和输出(练习 2.1, 2.3)。
  2. 给你一个功能需求,让你自己设计一张电路图(练习 2.4, 2.6)。
  3. 给你一个黑盒电路,让你反向推导出它的电路图(类似 DPDA → DCFG 的思想)。
  4. 证明某类电路组合(如串联、并联)后的功能特性(闭包性质)。
  5. 将一张复杂的电路图,转化为一种标准的、易于分析的布局(乔姆斯基范式)。

5行间公式索引

  1. 一个有歧义的、用于平衡括号的文法示例

$$ \begin{aligned} & S \rightarrow T \dashv \\ & T \rightarrow T T|(T)| \varepsilon \end{aligned} $$

  1. 与上一个文法等价的、无歧义的(确定性)文法示例

$$ \begin{aligned} & S \rightarrow T \dashv \\ & T \rightarrow T(T) \mid \varepsilon . \end{aligned} $$

  1. 一个在文法G中从开始变量到字符串w的最右推导序列

$$ A_{q_{0}, q_{\text {accept}}}=v_{0} \Rightarrow v_{1} \Rightarrow \cdots \Rightarrow v_{i} \Rightarrow \cdots \Rightarrow v_{k}=w $$

  1. 为证明目的,对DPDA的转移函数进行的扩展,使其能“读取”文法变量

$$ \delta\left(p, A_{p q}, \boldsymbol{\varepsilon}\right)=(q, \boldsymbol{\varepsilon}) $$

  1. 一个用于演示LR(1)优势的算术表达式文法

$$ \begin{aligned} & S \rightarrow E-1 \\ & E \rightarrow E+T \mid T \\ & T \rightarrow T \times \text{a} \mid \text{a} \end{aligned} $$

  1. 练习2.1中给出的标准算术表达式文法G4

$$ \begin{aligned} & E \rightarrow E+T \mid T \\ & T \rightarrow T \times F \mid F \\ & F \rightarrow(E) \mid \text{a} \end{aligned} $$

  1. 练习2.3中给出的一个上下文无关文法G,用于考察基本概念

$$ \begin{aligned} R & \rightarrow X R X \mid S \\ S & \rightarrow \mathrm{a} T \mathrm{~b} \mid \mathrm{b} T \mathrm{a} \\ T & \rightarrow X T X|X| \varepsilon \\ X & \rightarrow \mathrm{a} \mid \mathrm{b} \end{aligned} $$

  1. 练习2.9中给出的语言A的定义

$$ A=\left\{\mathrm{a}^{i} \mathrm{~b}^{j} \mathrm{c}^{k} \mid i=j \text{ 或 } j=k \text{ 其中 } i, j, k \geq 0\right\} . $$

  1. 练习2.13中给出的一个上下文无关文法G

$$ \begin{aligned} & S \rightarrow T T \mid U \\ & T \rightarrow 0 T|T 0| \# \\ & U \rightarrow 0 U 00 \mid \# \end{aligned} $$

  1. 练习2.14中给出的一个用于转换为乔姆斯基范式的CFG

$$ \begin{aligned} & A \rightarrow B A B|B| \varepsilon \\ & B \rightarrow 00 \mid \varepsilon \end{aligned} $$

6最终检查清单

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