11. 2.2 下推自动机
📜 [原文1]
在本节中,我们介绍了一种新型的计算模型,称为下推自动机。这些自动机类似于非确定性有限自动机,但它们有一个额外的组件,称为栈。栈提供了超越控制中可用有限内存的额外内存。栈允许下推自动机识别某些非正则语言。
📖 [逐步解释]
本段是下推自动机(Pushdown Automaton, PDA)的引言,旨在建立一个初步的概念。它将新概念与我们已经了解的有限自动机进行对比,并点出其核心区别与能力提升。
- “一种新型的计算模型,称为下推自动机”:
- 计算模型是一个数学上的抽象概念,它描述了计算机如何解决问题。我们之前学习的有限自动机(FA)就是一种简单的计算模型。图灵机是更强大的计算模型。这里的下推自动机是介于两者之间的一种模型。它定义了一套规则和组件,用来精确地描述一类计算过程。
- “这些自动机类似于非确定性有限自动机”:
- 这句话是在告诉我们,下推自动机不是一个全新的、凭空出现的概念,而是建立在非确定性有限自动机(Nondeterministic Finite Automaton, NFA)基础之上的。
- 相似之处在于:它们都有状态(States)、状态转移(Transitions)、一个输入带(Input Tape)和一个读头(Read Head)。它们都是从一个开始状态出发,根据当前状态和读取到的输入符号来决定下一步要进入哪个状态。非确定性意味着在某个状态下,对于同一个输入,可能有多条路径可以选择。
- “但它们有一个额外的组件,称为栈”:
- 这是下推自动机与有限自动机最根本、最重要的区别。栈(Stack)是一种数据结构,你可以把它想象成一个只能在一端操作的容器(比如一个桶或一叠盘子)。
- 这个栈为自动机提供了一种内存。
- “栈提供了超越控制中可用有限内存的额外内存”:
- 有限自动机的“内存”仅仅是它的状态。因为状态的数量是有限的,所以它的记忆能力也是有限的。它只能记住有限种模式或信息。例如,一个有5个状态的FA,最多只能区分5种不同的情况。
- 下推自动机的栈是无限深的。这意味着它理论上可以存储无限多的信息。这就克服了有限自动机的根本局限性。“超越控制中可用有限内存”指的就是超越了由有限状态所提供的内存。
- “栈允许下推自动机识别某些非正则语言”:
- 这是引入栈这个额外组件的直接结果和最终目的。我们知道,有限自动机只能识别正则语言。对于像 $L = \{0^n1^n \mid n \ge 0\}$ 这样的语言(0的个数必须和1的个数相等),有限自动机是无能为力的,因为它需要“计数”,而它的有限状态无法记录任意大的数量 $n$。
- 下推自动机因为有了无限栈,就可以解决这个问题。它可以把所有读到的0都“压入”栈中,然后每读到一个1,就从栈中“弹出”一个0。最后检查栈是否为空,就能判断0和1的数量是否相等。因此,下-推自动机能识别的语言集合(即上下文无关语言)比正则语言更大。
💡 [数值示例]
- 示例1 (正则语言): 考虑语言 $L_1 = \{ \text{所有以'1'结尾的01字符串} \}$。一个有限自动机可以轻松识别它。它只需要两个状态:一个“非结尾1”状态和一个“结尾1”状态。它不需要额外的内存来记录之前看到了什么,只需要知道最后一个符号是什么。
- 示例2 (非正则语言): 考虑语言 $L_2 = \{0^n1^n \mid n \ge 0\}$。
- 输入 "0011" ($n=2$): 有限自动机会迷失。当它读完两个'0'后,它无法“记住”自己看到了两个'0'。当它开始读'1'时,它不知道需要匹配多少个。
- 下推自动机则可以:
- 读第一个'0',将一个符号(比如'0')压入 栈。栈: [0]
- 读第二个'0',再压入一个'0'。栈: [0, 0]
- 读第一个'1',弹出一个'0'。栈: [0]
- 读第二个'1',再弹出一个'0'。栈: [] (空了)
- 输入结束,栈也空了。接受!
- 读第一个'0',压栈。栈: [0]
- 读第二个'0',压栈。栈: [0, 0]
- 读第一个'1',弹栈。栈: [0]
- 输入结束,但栈不为空。拒绝!
⚠️ [易错点]
- 易错点: 混淆有限自动机的“状态”和下推自动机的“栈”。状态是有限的,是控制逻辑的一部分。栈是无限的,是数据存储部分。PDA的状态数量本身仍然是有限的。
- 边界情况:
- 栈是空的,但还想弹出(pop)。这通常会导致计算分支的“死亡”或失败。
- 输入结束了,但栈还没空。对于某些语言(如 $0^n1^n$),这意味着拒绝。
- 栈空了,但输入还没结束。这也可能导致拒绝。
📝 [总结]
本段引入了下推自动机(PDA),它是在非确定性有限自动机(NFA)的基础上增加了一个无限栈作为内存。这个栈使得PDA能够存储和检索信息,从而识别比正则语言更复杂的语言,即非正则语言。
🎯 [存在目的]
本段的目的是建立从有限自动机到下推自动机的桥梁。它通过类比和对比,清晰地指出了PDA的核心新特性(栈)以及这个新特性带来的能力提升(识别非正则语言),为后续深入学习PDA的形式化定义和工作原理奠定基础。
🧠 [直觉心智模型]
你可以把有限自动机想象成一个只有短期记忆的人,他只能记住自己“当前在哪种情绪下”(状态)。而下推自动机则是同一个人,但给了他一叠无限的便签纸(栈)。他可以在纸上写东西(压栈),也可以撕掉最上面一张纸(弹栈),这使得他能够记录任意多的信息,比如他数了多少只羊。
💭 [直观想象]
想象一个在铁轨上行进的玩具火车(读头),铁轨上铺着一串串珠子(输入字符串)。火车旁边有一个弹簧加载的盘子架(栈)。
- 有限自动机: 火车只能根据它当前在哪个站点(状态)和看到的下一个珠子颜色(输入符号)来决定去往哪个站点。它没有地方放盘子。
- 下推自动机: 火车不仅可以看站点和珠子,还可以操作盘子架。例如,看到一个红色珠子,它就放一个红盘子到架子上(压栈);看到一个蓝色珠子,它就从架子上拿走一个盘子(弹栈)。这使得火车可以执行更复杂的任务,比如检查红蓝珠子的数量是否匹配。
📜 [原文2]
下推自动机在能力上与上下文无关文法等价。这种等价性非常有用,因为它为我们证明一种语言是上下文无关的提供了两种选择。我们可以提供生成它的上下文无关文法,也可以提供识别它的下推自动机。某些语言更容易用生成器来描述,而另一些则更容易用识别器来描述。
📖 [逐步解释]
这一段揭示了本章一个核心的理论结果,即下推自动机(PDA)和上下文无关文法(Context-Free Grammar, CFG)之间的深刻联系。
- “下推自动机在能力上与上下文无关文法等价”:
- 这是一个非常强有力的声明,是计算理论中的一个基石性定理。
- “能力上等价”意味着:对于任何一个可以用上下文无关文法描述的语言(即上下文无关语言),都存在一个下推自动机能够识别它。反之,对于任何一个可以被下推自动机识别的语言,也都存在一个上下文无关文法能够生成它。
- 它们是描述同一类语言集合(上下文无关语言集)的两种不同工具。一个是生成式的(CFG),一个是识别式的(PDA)。
- “这种等价性非常有用,因为它为我们证明一种语言是上下文无关的提供了两种选择”:
- 这解释了上述理论等价性的实践意义。当我们想证明一个语言 $L$ 属于上下文无关语言这个类别时,我们不必局限于一种方法。
- 选择1:构造一个CFG。我们要设计一套产生式规则,这套规则能且只能生成语言 $L$ 中的所有字符串。
- 选择2:构造一个PDA。我们要设计一个下推自动机,这个自动机能且只能接受语言 $L$ 中的所有字符串。
- 拥有两种工具就像一个木匠既有锤子又有螺丝刀,可以根据具体情况选择最称手的工具。
- “某些语言更容易用生成器来描述,而另一些则更容易用识别器来描述”:
- 这里给出了为什么拥有两种选择是件好事。
- 生成器(CFG)的优势: 对于具有递归、嵌套结构的语言,使用文法来描述通常非常自然和直观。比如编程语言的表达式、if-else 结构等。
- 识别器(PDA)的优势: 对于需要“匹配”或“计数”的语言,使用自动机的思路来描述通常更直接。比如前面提到的 $\{0^n1^n\}$,用PDA的“压栈-弹栈”模型来描述就非常清晰。
💡 [数值示例]
- 示例1 (更适合用CFG描述):
- 语言: 算术表达式,如 (a + b) * c。
- CFG描述:
- expr -> expr + term | expr - term | term
- term -> term * factor | term / factor | factor
- factor -> ( expr ) | ID
- 这个文法清晰地表达了表达式的递归和优先级结构。
- PDA描述: 构造一个识别这种表达式的PDA会相对复杂,需要处理运算符优先级、括号匹配等,虽然可行,但不直观。
- 示例2 (更适合用PDA描述):
- 语言: $L = \{w\#w^R \mid w \in \{0,1\}^*\}$,其中 $w^R$ 是 $w$ 的反转。例如 "011#110"。
- PDA描述:
- 读到 # 之前的所有符号,依次压栈。
- 读到 #。
- 读到 # 之后的所有符号,每读一个,就和栈顶符号比较,如果匹配,就弹栈。
- 如果所有符号都完美匹配直到输入结束且栈空,则接受。
- 这个识别过程非常机械和直接。
- CFG描述: 也可以构造,但可能不那么一目了然。
- S -> 0S0 | 1S1 | #
- 这个文法通过在中心 # 的两侧同时生成相同的符号来工作,也很好地体现了回文结构。这个例子实际上用两种方式描述都比较直观。但对于 $0^n1^n$ 这种,PDA的描述更符合我们解决问题的思考过程。
⚠️ [易错点]
- 易错点: 认为“等价”意味着CFG和PDA是一回事。它们不是一回事,一个是文法(一套规则),一个是自动机(一个机器模型)。“等价”指的是它们的表达能力(power)相同,即它们定义了相同的语言类别。
- 不要忘记双向性: 等价性是双向的。能用CFG生成的,就能用PDA识别;能用PDA识别的,就能用CFG生成。后面的章节会证明这一点。
📝 [总结]
本段的核心思想是:下推自动机和上下文无关文法是等价的计算工具,它们都能且仅能描述上下文无关语言。这为我们提供了一个灵活的选择,可以根据语言的特性,选择文法(生成器)或自动机(识别器)这两种不同但等价的途径来证明一个语言是上下文无关的。
🎯 [存在目的]
本段的目的是为了建立上下文无关语言的两个核心表示(文法和自动机)之间的联系,强调它们在理论上的等价性和在实践中的互补性。这为整个章节的学习设定了基调:我们将看到这两种模型如何相互转换,从而加深对上下文无关语言本质的理解。
[直觉心-智模型]
想象你要向别人描述“什么是合法的英文句子”。
- 方法一 (CFG - 生成器): 你可以给出一套语法规则,比如“一个句子由主语、谓语、宾语构成”,“主语可以是一个名词短语”,等等。别人可以用这套规则“造出”合法的句子。
- 方法二 (PDA - 识别器): 你可以设计一个流程图,告诉别人如何一步步检查一个给定的句子。比如“先找主语,找到了就在本子上做个记号,再找谓语...”。别人可以用这个流程图“验证”一个句子是否合法。
这两种方法不同,但最终定义了同一批“合法的英文句子”。
💭 [直观想象]
- 上下文无关文法 (CFG): 像一本菜谱。它告诉你如何从最基本的原料(终结符)和半成品(变量)开始,一步步组合(产生式),最终做出一道道合法的菜(语言中的字符串)。
- 下推自动机 (PDA): 像一个食品安全检查员。他有一个清单(状态和转移)和一个篮子(栈)。他看着传送带上的一盘菜(输入字符串),根据清单上的步骤,往篮子里放东西或者从篮子里取东西。最后,他根据篮子里的情况和清单的最终步骤,给这盘菜盖上“合格”或“不合格”的章。
📜 [原文3]
下图是有限自动机的示意图。控制表示状态和转移函数,纸带包含输入字符串,箭头表示输入磁头,指向要读取的下一个输入符号。

图 2.11
有限自动机示意图
📖 [逐步解释]
这一部分通过一张图示,回顾了有限自动机(FA)的基本构造,为接下来引出下推自动机的示意图做铺垫。
- “下图是有限自动机示意图”: 这明确了图像的内容,是一个高度抽象的、表示有限自动机工作方式的框图。
- “控制表示状态和转移函数”:
- 图中的 "FINITE CONTROL"(有限控制)是自动机的大脑。
- 它内部包含了自动机的所有状态(比如 $q_0, q_1, \dots, q_n$)。因为状态数量是有限的,所以这个控制单元被称为“有限控制”。
- 它还包含了转移函数 $\delta$。转移函数是一套规则,它规定了在某个状态下,当输入磁头读取到某个符号时,自动机应该转移到哪个新状态。例如,$\delta(q_i, a) = q_j$。
- “纸带包含输入字符串”:
- 图中的 "INPUT TAPE"(输入带)是用来存放待处理的字符串的。
- 这个纸带被划分成一个个的小格子,每个格子里存放一个输入符号。例如,如果输入是 "aba",那么纸带上就是 a | b | a。
- 通常我们假设纸带是只读的,并且磁头只能从左向右移动,不能后退。
- “箭头表示输入磁头,指向要读取的下一个输入符号”:
- 图中的向下箭头就是 "INPUT HEAD"(输入磁头)。
- 它的作用是在任意时刻指向输入带上的一个格子,表示这是自动机当前正要处理的符号。
- 每当自动机完成一次状态转移,磁头通常会向右移动一格,准备读取下一个符号。
💡 [数值示例]
- 示例: 假设有一个识别语言 $\{a^n \mid n \text{ is even}\}$ 的FA,输入字符串为 "aa"。
- 初始状态: 有限控制处于开始状态 $q_0$。输入带为 a | a。输入磁头指向第一个 'a'。
- 第一步: 有限控制根据规则 $\delta(q_0, a) = q_1$ 决定转移到状态 $q_1$。输入磁头向右移动一格,指向第二个 'a'。
- 第二步: 有限控制根据规则 $\delta(q_1, a) = q_0$ 决定转移回状态 $q_0$。输入磁头再向右移动一格,移出输入字符串的范围。
- 结束: 输入读取完毕。自动机停在状态 $q_0$。如果 $q_0$ 是接受状态,则字符串 "aa" 被接受。
⚠️ [易错点]
- 易错点: 将示意图中的“有限控制”理解为一个物理上的CPU。它实际上是一个数学上的抽象,代表了状态集合和转移规则的集合。
- 边界情况:
- 输入为空字符串 $\varepsilon$: 磁头一开始就位于输入字符串的末尾。自动机不做任何移动,直接根据其开始状态是否为接受状态来决定接受或拒绝。
- 输入读取完毕: 当磁头移过最后一个符号后,计算结束。此时自动机所在的状态决定了整个字符串是否被接受。
📝 [总结]
本段通过一个示意图,简明扼要地回顾了有限自动机的三个核心组成部分:有限控制(包含状态和转移规则)、输入带(存放字符串)和输入磁头(读取符号)。这为理解下推自动机的结构差异打下了基础。
🎯 [存在目的]
此处的目的是“温故知新”。通过展示一个熟悉的模型(FA),为紧接着引入一个更复杂的、在其基础上扩展的模型(PDA)建立一个清晰的对比参照物。读者可以立刻看出PDA比FA多出了什么组件。
[直觉心-智模型]
有限自动机就像一个只能“向前看”的机器人。它沿着一条写满指令的纸带前进,它的大脑(有限控制)里只有几条简单的规则:“如果我在A点看到红色,就去B点;如果我在B点看到蓝色,就去C点”。它没有记忆,走过就忘了。
💭 [直观想象]
想象你在玩一个简单的桌面游戏。
- 棋盘上的格子: 对应输入带上的符号。
- 你的棋子: 对应输入磁头。
- 游戏规则手册: 对应有限控制。手册上写着:“当你的棋子在X格,掷出骰子Y点,则移动到Z格”。
- 你的大脑状态: 对应自动机状态。
你只能根据手册、你当前的位置和骰子点数来移动,没有额外的纸笔来做记录。
📜 [原文4]
通过添加栈组件,我们获得了下推自动机的示意图,如下图所示。

图 2.12
下推自动机示意图
📖 [逐步解释]
这张图和前一张图(图2.11)形成了鲜明的对比,直观地展示了下推自动机(PDA)相比于有限自动机(FA)的核心增量。
- “通过添加栈组件”: 这句话点明了从 FA 到 PDA 的演化路径:PDA = FA + Stack。
- 分析图示:
- 保留部分: 和图2.11一样,图2.12中依然有 "FINITE CONTROL"(有限控制)、"INPUT TAPE"(输入带)和 "INPUT HEAD"(输入磁头)。这说明 PDA 继承了 FA 的基本框架。
- 新增部分: 图的左侧显著增加了一个名为 "STACK"(栈)的垂直组件。这个栈与有限控制单元相连。
- 交互: 有限控制单元现在不仅连接着输入磁头,还连接着栈。图示中的双向箭头表明,有限控制既可以向栈写入信息(压栈,Push),也可以从栈读取信息(弹栈,Pop)。
- 工作流程的变化:
- 在 FA 中,转移函数 $\delta$ 的决策依据仅仅是:当前状态 和 当前输入符号。即 (currentState, inputSymbol) -> nextState。
- 在 PDA 中,转移函数 $\delta$ 的决策依据变成了三个:当前状态、当前输入符号 和 栈顶符号。即 (currentState, inputSymbol, stackTopSymbol) -> (nextState, newStackTop)。
- PDA 的每一步动作,不仅可以改变状态,还可以改变栈的内容。这使得自动机的行为变得更加丰富和强大。
💡 [数值示例]
- 示例: 再次考虑识别 $L = \{0^n1^n \mid n \ge 0\}$ 的PDA,输入为 "01"。
- 初始: 有限控制在 $q_0$,输入磁头指向 '0',栈为空。
- 第一步:
- 决策依据: (状态=q_0, 输入='0', 栈顶=空)。
- 动作: 转移到状态 $q_1$,并向栈中压入一个符号 '$' 和 '0'。
- 结果: 状态变为 $q_1$,磁头右移,栈内容为 `['0', '$']` (假设'$'是栈底标记)。
- 第二步:
- 决策依据: (状态=q_1, 输入='1', 栈顶='0')。
- 动作: 转移到状态 $q_2$,并从栈中弹出 '0'。
- 结果: 状态变为 $q_2$,磁头右移,栈内容为 ['$']。
- 第三步:
- 决策依据: (状态=q_2, 输入=结束, 栈顶='$')。
- 动作: 转移到接受状态 $q_f$,弹出 '$'。
- 结果: 状态变为 $q_f$,计算结束,栈为空。接受字符串。
⚠️ [易错点]
- 易错点: 认为栈是独立工作的。栈本身没有智能,它完全受有限控制单元的操控。所有的压栈和弹栈操作都是由转移函数决定的。
- 信息流: 要清楚PDA决策的信息来源是三方的:状态、输入和栈顶。
📝 [总结]
本段通过一个清晰的示意图,展示了下推自动机的结构。它是在有限自动机的基础上,增加了一个与有限控制单元交互的栈。这个栈作为一种内存设备,使得PDA的决策和行为比FA更加复杂和强大。
🎯 [存在目的]
此图的目的是以最直观的方式,将下推自动机这个新模型的物理结构印在读者的脑海里。通过与上一张图的直接对比,读者能够瞬间抓住PDA的精髓——栈的存在及其与控制单元的连接。这是后续理解其形式化定义和工作原理的视觉基础。
🧠 [直觉心智模型]
如果说有限自动机是一个只能“活在当下”的决策者,那么下推自动机就是一个带了“记事本”的决策者。
- 输入是不断涌来的新情况。
- 状态是他当前的心情。
- 栈(记事本)是他记录过去信息的工具。他可以在记事本最上面写一行(压栈),也可以看一眼最上面一行并把它划掉(弹栈)。
他的下一个决定不仅取决于心情和新情况,还取决于他记事本上最新写的东西。
💭 [直观想象]
想象一个厨师在准备一道复杂的菜肴。
- 菜谱: 有限控制。
- 传送带上的食材: 输入带。
- 厨师的手: 输入磁头。
- 一个用来临时存放和混合调料的碗: 栈。
厨师不仅要根据菜谱和他刚拿到的食材来决定下一步做什么,他还要看看碗里已经有了什么调料(栈顶),然后决定是再加一点(压栈),还是把碗里的东西用掉(弹栈)。这个碗给了他处理复杂配比的能力。
📜 [原文5]
下推自动机(PDA)可以在栈上写入符号,并在稍后读回它们。写入一个符号会“向下推”栈上的所有其他符号。在任何时候,都可以读取并移除栈顶的符号。然后,剩余的符号会向上移动。在栈上写入符号通常被称为压栈(pushing the symbol),而移除符号则被称为弹栈(popping it)。请注意,对栈的所有访问,无论是读取还是写入,都只能在顶部进行。换句话-说,栈是一种“后进先出”的存储设备。如果某些信息被写入栈,然后又写入了额外的信息,那么较早的信息在较晚的信息被移除之前是不可访问的。
📖 [逐步解释]
这一段详细解释了下推自动机核心组件——栈(Stack)的工作原理和特性。
- “可以在栈上写入符号,并在稍后读回它们”:
- 这定义了栈的基本功能:存储和检索。PDA的控制单元可以把一些信息(以符号的形式)存入栈,以便在未来的某个时刻使用这些信息。
- “写入一个符号会‘向下推’栈上的所有其他符号”:
- 这是对压栈(Push)操作的形象描述。想象一摞盘子,当你放一个新盘子在最上面时,原来所有的盘子都被“向下推”了一点位置。在数据结构中,这意味着新元素被放置在了集合的“顶部”。
- “在任何时候,都可以读取并移除栈顶的符号。然后,剩余的符号会向上移动”:
- 这是对弹栈(Pop)操作的描述。你只能拿走最上面的那个盘子。一旦你拿走了它,它下面的那个盘子就成了新的最顶部的盘子,仿佛“向上移动”了。
- “读取并移除”是一个原子操作。你看一眼栈顶符号是什么,然后这个符号就从栈中消失了。
- “在栈上写入符号通常被称为压栈(pushing the symbol),而移除符号则被称为弹栈(popping it)”:
- 这里给出了这两个基本操作的标准术语:Push 和 Pop。
- “请注意,对栈的所有访问,无论是读取还是写入,都只能在顶部进行”:
- 这是栈最根本的约束。你不能“伸手”到栈的中间或底部去拿或放一个符号。所有的操作都严格限制在栈的唯一一个访问点——栈顶(Top of the Stack)。
- “换句话说,栈是一种‘后进先出’的存储设备”:
- “后进先出” (Last-In, First-Out, LIFO) 是对栈行为的高度概括。
- 后进 (Last-In): 最晚被压入栈的元素,位于栈的最顶部。
- 先出 (First-Out): 当执行弹栈操作时,这个最晚被放进去的元素,总是第一个被取出来。
- “如果某些信息被写入栈,然后又写入了额外的信息,那么较早的信息在较晚的信息被移除之前是不可访问的”:
- 这是 LIFO 原则的直接推论。你把笔记A放进箱子,再把笔记B放在A上面。在你拿出笔记B之前,你是绝对拿不到笔记A的。这强调了栈内信息的访问顺序是严格受限的。
💡 [数值示例]
- 初始状态: 栈为空 []。
- push('A'): 栈变为 ['A']。'A'在栈顶。
- push('B'): 'A'被向下推,'B'成为新的栈顶。栈变为 ['B', 'A']。
- push('C'): 'B'和'A'被向下推,'C'成为新的栈顶。栈变为 ['C', 'B', 'A']。
- pop(): 读取并移除栈顶的'C'。'B'成为新的栈顶。栈变为 ['B', 'A']。返回值为 'C'。
- pop(): 读取并移除栈顶的'B'。'A'成为新的栈顶。栈变为 ['A']。返回值为 'B'。
- pop(): 读取并移除栈顶的'A'。栈变为空。栈变为 []。返回值为 'A'。
- 可以看到,压栈顺序是 A, B, C,而弹栈顺序是 C, B, A,完全相反。
- 示例2: 语言 $\{0^n1^n\}$ 的应用
- 输入 "0011"。
- push('0'): 栈 ['0']
- push('0'): 栈 ['0', '0'] (第二个'0'在顶)
- pop(): 匹配第一个'1',弹出一个'0'。栈 ['0']
- pop(): 匹配第二个'1',弹出一个'0'。栈 []
- 较早进入的'0'(第一个'0')确实是在较晚进入的'0'(第二个'0')被处理之后,才被处理的。但因为我们处理的是对称的'1',所以看起来像是先进先出,这只是巧合。真正的LIFO特性在处理回文语言 $\{ww^R\}$ 时更为明显。
⚠️ [易错点]
- 易错点: 误以为可以查看栈内部的元素。PDA的转移函数只能根据栈顶的符号做决策,它对栈的第2、第3...个元素是“盲人”。
- 边界情况:
- 对空栈执行 pop: 这是一个无效操作,通常在PDA的计算模型中,这会导致该计算分支终止且不接受。
- 对空栈执行 push: 这是一个完全合法的操作,栈中将包含这第一个元素。
📝 [总结]
本段详细阐述了栈作为一种“后进先出”(LIFO)数据结构的核心工作原理。它明确了两个基本操作:压栈(Push)和弹栈(Pop),并强调所有操作都只能在栈顶进行,这导致了先存入的信息必须等到后存入的信息被全部取出后才能被访问。
🎯 [存在目的]
本段的目的是为了确保读者对PDA的这个核心新组件——栈——有一个精确、无歧-义的理解。在介绍PDA的形式化定义之前,必须先讲清楚栈的行为,因为PDA的所有扩展能力都源于对栈的这种 LIFO 式操作。
🧠 [直觉心智模型]
栈就像一个只能从顶部开口的、很深的井。
- 压栈 (Push): 往井里扔一块石头。它会落在之前所有石头的最上面。
- 弹栈 (Pop): 从井口用绳子吊上一块石头。你只能吊起最上面的那一块。
你看不见下面有多少块石头,也无法选择吊哪一块,只能操作最顶上的一块。
💭 [直观想象]
最经典的例子是浏览器的“后退”按钮。
- 你访问了页面A。
- 然后点击链接去了页面B。push('A')
- 又从页面B去了页面C。push('B')
你的浏览历史栈现在是 ['B', 'A'](C是当前页面)。
- 当你点击“后退”时 (pop()),它会取出栈顶的 'B',然后跳转到页面B。历史栈现在是 ['A']。
- 再点击“后退” (pop()),它会取出栈顶的 'A',然后跳转到页面A。历史栈现在是 []。
- 最后访问的页面,最先在“后退”时返回。这就是后进先出。
📜 [原文6]
自助餐厅餐具柜上的盘子说明了栈。这叠盘子放在一个弹簧上,因此当一个新盘子放在栈顶时,下面的盘子会向下移动。下推自动机上的栈就像一叠盘子,每个盘子上都写有一个符号。
📖 [逐步解释]
这一段用一个非常生动和常见的现实生活例子——自助餐厅的盘子分发器——来进一步巩固对栈(Stack)工作原理的理解。
- “自助餐厅餐具柜上的盘子说明了栈”:
- 直接点明类比物。作者认为这个例子能极好地阐释栈的LIFO(后进先出)特性。
- “这叠盘子放在一个弹簧上,因此当一个新盘子放在栈顶时,下面的盘子会向下移动”:
- 这描述了压栈(Push)操作。
- 新盘子: 对应要压入栈的新符号。
- 放在栈顶: 对应压栈操作,新元素成为栈顶。
- 弹簧: 这是这个类比的精髓。它确保了盘子堆的顶部永远保持在同一个高度,方便取用。在数据结构中,这意味着我们总是有个固定的指针或引用指向栈顶,而不需要关心栈当前有多深。
- 下面的盘子向下移动: 对应栈中已有的元素在逻辑上被“推”得更深了。
- “下推自动机上的栈就像一叠盘子,每个盘子上都写有一个符号”:
- 这里将类比物与我们的主题下推自动机直接关联起来。
- 一叠盘子: 对应PDA中的栈。
- 每个盘子: 对应栈中的一个存储单元。
- 盘子上写的符号: 对应栈中存储的栈字母表中的符号(例如 '0', 'A', '$' 等)。PDA通过这些符号来记录信息。
💡 [数值示例]
- 场景: 一个盘子分发器,和一个要存入的符号序列 A, B, C。
- 初始: 分发器是空的。
- 操作 push('A'):
- 现实: 你拿一个写着'A'的盘子,放到分发器里。它沉下去一点。
- 数据结构: 栈变为 ['A']。
- 操作 push('B'):
- 现实: 你再拿一个写着'B'的盘子,放到'A'盘子上面。'A'和'B'一起又下沉了一点,但'B'在最上面露着。
- 数据结构: 栈变为 ['B', 'A']。
- 操作 pop():
- 现实: 你从分发器顶部拿走一个盘子,你拿到的是写着'B'的那个。拿走后,弹簧把剩下的'A'盘子顶了上来。
- 数据结构: 栈顶元素'B'被移除,栈变为 ['A']。
⚠️ [易错点]
- 类比的局限性: 这个类比非常适合解释 LIFO,但也有局限。真实的盘子分发器容量是有限的,而理论上PDA的栈是无限深的。此外,你不能对一个空的分发器“拿走”盘子。
📝 [总结]
本段使用自助餐厅的弹簧盘子分发器作为栈的直观类比。新盘子(新符号)被压入时,旧盘子下沉;取盘子(弹栈)时,只能拿最上面的一个。这个例子生动地体现了栈的“后进先出”(LIFO)特性。
🎯 [存在目的]
此段的目的是提供一个极其容易理解的、非技术的现实世界模型,来帮助读者内化栈这个抽象数据结构的行为。通过诉诸读者的日常经验,可以消除对这个概念的陌生感和畏惧感,使得后续的形式化学习更加顺畅。
[直觉心-智模型]
这个例子本身就是一个完美的直觉心智模型。当你想到栈时,脑海中直接浮现出一个盘子分发器的画面,它的所有行为(Push, Pop, Top)都和这个画面一一对应。
💭 [直观想象]
想象一下你正在整理一堆书,准备把它们放进一个很窄的箱子里。
- 你先把《历史》放进去(push(历史))。
- 再把《数学》放在《历史》上面(push(数学))。
- 最后把《物理》放在最上面(push(物理))。
- 现在箱子里从上到下是:《物理》,《数学》,《历史》。
- 当你想拿书出来看时,你必须先把《物理》拿出来(pop() -> 物理),然后才能拿到《数学》(pop() -> 数学),最后才能拿到《历史》(pop() -> 历史)。你放书的顺序和你取书的顺序是相反的。
📜 [原文7]
栈之所以有价值,是因为它可以存储无限量的信息。回想一下,有限自动机无法识别语言 $\left\{0^{n} 1^{n} \mid n \geq 0\right\}$,因为它无法在其有限内存中存储非常大的数字。PDA能够识别这种语言,因为它可以使用其栈来存储它所看到的 0 的数量。因此,栈的无限性允许 PDA 存储无限大的数字。下面的非正式描述展示了这种语言的自动机是如何工作的。
📖 [逐步解释]
本段阐明了栈的核心价值所在——无限内存,并用一个经典的例子 $\{0^n1^n\}$ 来具体说明这个价值是如何体现在语言识别能力上的。
- “栈之所以有价值,是因为它可以存储无限量的信息”:
- 这是对栈作用的根本性论断。在计算理论中,一个模型的“能力”很大程度上取决于其“内存”的类型和大小。
- 无限量是关键。与有限自动机的有限状态(有限内存)形成鲜明对比。
- “回想一下,有限自动机无法识别语言 $\left\{0^{n} 1^{n} \mid n \geq 0\right\}$,因为它无法在其有限内存中存储非常大的数字”:
- 这是一个重要的回顾,用一个我们熟悉的失败案例来反衬PDA的成功。
- 为什么FA无法识别? 要验证一个字符串是否属于 $\{0^n1^n\}$,机器必须确保'0'的数量和'1'的数量完全相等。这意味着机器在读完所有的'0'之后,必须“记住”到底读了多少个'0'。如果 $n$ 可以是任意大的非负整数(例如 $n=10^{100}$),那么需要记忆的状态也需要是无限的。但有限自动机的状态数是有限的(比如 $k$ 个),它最多只能精确地区分 $k$ 种不同的计数值。一旦 $n > k$,FA就会“混淆”,无法区分是 $n$ 个'0'还是 $n+1$ 个'0'。这就是著名的“泵引理”所揭示的根本局限。
- “PDA能够识别这种语言,因为它可以使用其栈来存储它所看到的 0 的数量”:
- 这里给出了PDA成功的秘诀。PDA不需要用有限的状态去“硬记”数字 $n$。
- 它采用了一种更聪明的方式:映射。它将看到的'0'的数量,映射为栈中存储的符号的数量。来一个'0',我就往栈里放一个标记物。
- 因为栈是无限的,所以无论 $n$ 有多大,栈都有足够的空间来存放 $n$ 个标记物。
- “因此,栈的无限性允许 PDA 存储无限大的数字”:
- 这是对上一句的总结和升华。PDA通过将数字(计数)转换为栈的深度,间接地实现了存储无限大数字的能力。
- “下面的非正式描述展示了这种语言的自动机是如何工作的”:
- 预告了接下来将要给出的、不涉及形式化定义的、易于理解的算法描述。
💡 [数值示例]
- FA的失败 (n=5): 假设一个FA有4个状态,它想用来计数。
- 读第1个'0' -> 状态1
- 读第2个'0' -> 状态2
- 读第3个'0' -> 状态3
- 读第4个'0' -> 状态4
- 读第5个'0' -> 没有新状态了! 机器必须回到一个已经用过的状态,比如回到状态1。此时,机器就“忘记”了自己是读了1个'0'还是5个'0',因为它都停在状态1。
- PDA的成功 (n=5):
- 读第1个'0' -> 栈: [X]
- 读第2个'0' -> 栈: [X, X]
- 读第3个'0' -> 栈: [X, X, X]
- 读第4个'0' -> 栈: [X, X, X, X]
- 读第5个'0' -> 栈: [X, X, X, X, X]
- 此时PDA的状态可能一直没变,但它的栈忠实地记录了'0'的数量。接下来读'1'时,栈里有5个'X'可供消耗。
⚠️ [易错点]
- 易错点: 认为PDA的状态也是无限的。不是的,PDA的状态集 $Q$ 仍然是有限的。它的无限能力完全来自于栈。
- 边界情况: $n=0$ 的情况。输入是空字符串 $\varepsilon$。一个设计良好的PDA应该能够正确处理这种情况,即不做任何压栈/弹栈操作,直接从开始状态转移到接受状态。
📝 [总结]
本段的核心论点是:栈的无限性是下推自动机超越有限自动机的关键。通过将计数问题转化为栈深度的变化,PDA可以“存储”任意大的数字,从而能够识别像 $\{0^n1^n\}$ 这样需要无限记忆能力的非正则语言。
🎯 [存在目的]
本段的目的是通过一个具体且经典的例子,将前面介绍的PDA的抽象结构和能力,与解决一个实际的、FA无法解决的问题联系起来。这使得栈的“价值”不再是空谈,而是有了实实在在的体现,极大地增强了读者对引入PDA必要性的认同感。
🧠 [直觉心智模型]
想象你要用两种方法数进入房间的人数。
- 方法一 (FA): 你用手指头数。你只有10个手指,所以最多只能数到10。如果来了第11个人,你就“内存溢出”了,不知道现在是多少人了。
- 方法二 (PDA): 你旁边有一个筐(栈),还有一堆石头。每进来一个人,你就往筐里放一块石头。因为石头有无限多,筐也无限大,所以无论来多少人,你都能准确记录。想知道来了多少人,数数筐里的石头就行了。这就是PDA的策略。
💭 [直观想象]
一个简单的对比:
- 有限自动机: 像一个只有开/关两种状态的灯。它只能记住“是”或“否”的二进制信息。
- 下推自动机: 像一个算盘。虽然算盘的控制很简单(上下拨动珠子),但因为珠子和档位足够多(栈是无限的),它可以表示非常大的数字。
📜 [原文8]
从输入中读取符号。每读取一个 0,就将其压栈。一旦看到 1,每读取一个 1 就从栈中弹栈一个 0。如果输入读取完成时栈恰好清空了 0,则接受输入。如果栈在 1 仍然存在时清空,或者 1 读取完成时栈仍然包含 0,或者在 1 之后输入中出现任何 0,则拒绝输入。
📖 [逐步解释]
这是对识别语言 $L = \{0^n1^n \mid n \ge 0\}$ 的PDA工作算法的非正式描述。它把整个过程分解为几个清晰的步骤。
- “从输入中读取符号”: 这是自动机的基本动作,输入磁头从左到右扫描字符串。
- “每读取一个 0,就将其压栈”:
- 这是算法的第一阶段,处理字符串的前半部分(0的部分)。
- “将其压栈”是一种简略说法,实际上是压入一个代表'0'的栈符号。这个栈符号可以是 '0' 本身,也可以是任何我们指定的特殊符号,比如 '$' 或者 'X'。
- 这个阶段,PDA像一个计数器,把看到的'0'累积在栈里。
- “一旦看到 1,每读取一个 1 就从栈中弹栈一个 0”:
- 这是算法的第二阶段,处理字符串的后半部分(1的部分)。
- “一旦看到 1” 标志着阶段的转换。PDA从“只读0”模式切换到“只读1”模式。
- “弹栈一个 0”:这里的 '0' 指的是第一阶段压入的那个代表'0'的栈符号。这个操作是在消耗之前累积的计数。
- “如果输入读取完成时栈恰好清空了 0,则接受输入”:
- 这是“成功”的条件。
- 输入读取完成: 输入磁头已经移过了最后一个符号。
- 栈恰好清空: 栈里不再有代表'0'的那些符号了。(可能还有一个特殊的栈底标记,后面会讲到)。
- 这个条件同时满足,意味着压栈的次数('0'的数量)和弹栈的次数('1'的数量)完全相等。因此,字符串的形式是 $0^n1^n$。
- “如果栈在 1 仍然存在时清空,或者 1 读取完成时栈仍然包含 0,或者在 1 之后输入中出现任何 0,则拒绝输入”:
- 这里列举了三种“失败”的条件。
- a) “栈在 1 仍然存在时清空”: 这意味着'0'的数量少于'1'的数量。比如输入 "011"。我们压栈一个'0',然后弹栈一个'0',此时栈空了,但输入带上还有一个'1'没读。这说明 $n < m$ in $0^n1^m$。拒绝。
- b) “1 读取完成时栈仍然包含 0”: 这意味着'0'的数量多于'1'的数量。比如输入 "001"。我们压栈两个'0',然后读一个'1'并弹栈一个'0'。此时输入结束了,但栈里还剩一个'0'。这说明 $n > m$ in $0^n1^m$。拒绝。
- c) “在 1 之后输入中出现任何 0”: 这破坏了 $0^n1^n$ 的格式要求(所有的0必须在所有的1之前)。比如输入 "010"。PDA在读到第二个'0'时,因为它已经进入了“读1”的模式,所以没有合法的转移路径。计算卡住,拒绝。
💡 [数值示例]
- 读 '0' -> push('X')。栈: [X]
- 读 '0' -> push('X')。栈: [X, X]
- 读 '1' -> pop()。栈: [X]
- 读 '1' -> pop()。栈: [] (空)
- 输入结束,栈空。 接受。
- 读 '0' -> push('X')。栈: [X]
- 读 '1' -> pop()。栈: [] (空)
- 读 '1' -> 此时PDA想执行pop(),但栈已经是空的了。这是一个失败条件 (a)。拒绝。
- 读 '0' -> push('X')。栈: [X]
- 读 '0' -> push('X')。栈: [X, X]
- 读 '1' -> pop()。栈: [X]
- 输入结束,但栈不为空。这是一个失败条件 (b)。拒绝。
- 读 '0' -> push('X')。栈: [X]
- 读 '1' -> pop()。栈: []
- 读 '0' -> PDA此时处于“匹配1”的阶段,看到'0'会没有定义好的转移,或者转移到“陷阱状态”。这是一个失败条件 (c)。拒绝。
⚠️ [易错点]
- 边界情况: $n=0$,输入为空字符串 $\varepsilon$
- PDA从开始状态启动。
- 它没有读到'0',所以不执行任何压栈。
- 它也没有读到'1',所以不执行任何弹栈。
- 输入直接结束,栈也一直为空。
- 根据成功条件,PDA应该接受空字符串。设计时必须考虑到这一点。
📝 [总结]
本段以算法步骤的形式,清晰地描述了下推自动机如何识别语言 $\{0^n1^n\}$。其核心策略是:用压栈操作来“累加”0的数量,用弹栈操作来“抵消”1的数量,最后通过检查栈是否恰好为空来判定0和1的数量是否相等,并同时检查字符串的格式是否正确。
🎯 [存在目的]
这段描述的目的是将前面关于PDA能力的抽象讨论,落实到一个具体、可操作的算法上。它为读者提供了一个清晰的、非形式化的心智模型,让他们明白PDA是如何利用栈来解决一个FA无法解决的问题的。这为后面理解PDA的形式化定义和状态转移图做好了铺-垫。
🧠 [直觉心智模型]
这就像一个会计在对账。
- 账本的左边(收入)记录了一系列的'0'。
- 账本的右边(支出)记录了一系列的'1'。
- 会计(PDA)的工作是核对收入和支出是否相等。
- 压栈: 每看到一笔收入'0',他就在草稿纸(栈)上画一个勾。
- 弹栈: 每看到一笔支出'1',他就划掉草稿纸上的一个勾。
- 接受: 对账结束时,如果草稿纸上所有的勾都正好被划掉了,说明账平了。
- 拒绝: 如果支出没完,勾就划完了(钱不够花);或者收入对完了,勾还有剩(钱没花完),都说明账不平。
💭 [直观想象]
想象你在搭积木,要搭一个左右对称的结构。
- 压栈: 你先用红色的积木搭左半边,每搭一块,你就把这块积木的编号写在一张纸条上,然后把纸条扔进一个桶里。
- 弹栈: 当你开始搭右半边时,你用蓝色的积木。每搭一块蓝色积木,你就从桶里摸出一张纸条,检查编号是否对应,然后扔掉纸条。
- 接受: 如果你搭完右半边时,桶里的纸条也正好用完了,说明你的结构是完美的对称。
- 拒绝: 如果桶里的纸条提前用完,或者积木搭完了纸条还有剩,都说明结构不对称。
📜 [原文9]
如前所述,下推自动机可能具有非确定性。确定性下推自动机和非确定性下推自动机在能力上并不等价。
📖 [逐步解释]
这一小段揭示了下推自动机(PDA)与其前辈有限自动机(FA)之间一个至关重要的理论差异。
- “如前所述,下推自动机可能具有非确定性”:
- 这句话重申了PDA模型的一个基本特性。像NFA一样,PDA的转移函数在某个情况下可以有多个选择。
- 一个非确定性的PDA(Nondeterministic Pushdown Automaton, NPDA)在面对 (当前状态, 当前输入, 栈顶符号) 的组合时,可能有多种合法的下一步动作,比如:
- 选择转移到状态 A 并压栈 X。
- 或者选择转移到状态 B 并弹栈 Y。
- 或者选择停在当前状态,不读输入,只操作栈。
- NPDA的计算可以看作是一棵计算树,只要其中有一条路径能够成功到达接受状态,输入的字符串就被接受。
- “确定性下推自动机和非确定性下推自动机在能力上并不等价”:
- 这是本段的核心,也是计算理论中一个非常重要的结论。
- 确定性下推自动机(Deterministic Pushdown Automaton, DPDA)在任何时候都只有一个唯一的、确定的下一步动作。它的转移函数对于任何合法的 (状态, 输入, 栈顶) 组合,最多只能映射到一个 (新状态, 新栈操作)。
- “能力上并不等价”意味着存在一些语言,它们可以被NPDA识别,但是不可能被任何DPDA识别。
- 这与有限自动机的情况形成了鲜明对比。我们知道,对于任何一个NFA,都存在一个等价的DFA。DFA和NFA在语言识别能力上是等价的,它们都只能识别正则语言。
- 但在下推自动机的世界里,非确定性赋予了模型更强的计算能力。
💡 [数值示例]
- 示例1 (DPDA可以识别的语言): $L_1 = \{0^n1^n \mid n \ge 0\}$。
- 这个语言的识别过程是确定性的。
- 当看到 '0' 时,唯一的动作就是压栈。
- 当第一次看到 '1' 时,唯一的动作就是切换模式并开始弹栈。
- 在“弹栈”模式下,看到 '1',唯一的动作就是弹栈。
- 整个过程中没有任何需要“猜测”的地方。因此,存在一个DPDA可以识别它。
- 示例2 (DPDA无法识别,但NPDA可以识别的语言): $L_2 = \{ww^R \mid w \in \{0,1\}^*\}$ (偶数长度的回文语言)。
- 为什么DPDA不行? 一个确定性的PDA在读取字符串时,必须在某个时刻明确地决定“前半部分w结束了,现在开始匹配后半部分w^R”。但是,它没有办法知道字符串的中点在哪里。如果输入是 "0110",中点在第二个'1'之后。如果输入是 "001100",中点在第三个'1'之后。DPDA无法“预见”未来,它不能说“我再读一个字符看看是不是中点”,因为它没有一个明确的标记(比如 #)来告诉它中点的位置。
- NPDA如何工作? 非确定性允许PDA“猜测”!在读取输入的每一步,NPDA都可以做出一个非确定性的选择:
- 猜测1: "还没到中点,继续读入并压栈"。
- 猜测2: "正好到中点了,从现在开始切换到匹配和弹栈模式"。
- 对于输入 "0110",NPDA会产生多条计算路径。其中一条正确的路径是:
- 读 '0' -> 压栈 (猜测不是中点)
- 读 '1' -> 压栈 (猜测不是中点)
- 此时栈是 ['1', '0']。自动机非确定性地猜测,"中点到了!"。
- 读下一个 '1' -> 弹栈'1',匹配成功。
- 读下一个 '0' -> 弹栈'0',匹配成功。
- 输入结束,栈空。接受!
- 其他的猜测路径(比如在第一个'0'后就猜测是中点)都会失败,但只要有一条成功路径,字符串就被接受。
⚠️ [易错点]
- 易错点: 将FA的等价性结论错误地应用到PDA上。必须牢记:DFA ~ NFA,但 DPDA !~ NPDA(~表示等价)。
- 关键区别: 非确定性在FA中是一种便利,可以用更少状态简洁地表达语言,但不会提升能力。在PDA中,非确定性是一种本质性的能力提升,它允许自动机探索多种可能性(比如“猜测”中点)。
📝 [总结]
本段指出了下推自动机的一个关键理论特性:确定性模型(DPDA)和非确定性模型(NPDA)在计算能力上是不等价的。非确定性赋予了NPDA识别某些DPDA无法识别的语言的能力,这与有限自动机中确定性和非确定性等价的情况截然不同。
🎯 [存在目的]
本段的目的是为了纠正读者可能从有限自动机学习中带过来的思维定势,并强调非确定性在下推自动机世界中的核心重要性。这解释了为什么后续的讨论(特别是与上下文无关文法的等价性证明)将主要围绕非确定性的PDA展开。
🧠 [直觉心智模型]
想象你在一个岔路口。
- 确定性自动机 (DFA/DPDA): 你面前只有一个路牌,明确地告诉你“直走”。你没有选择。
- 非确定性自动机 (NFA/NPDA): 你面前有多个路牌,“向左通往A地”,“向右通往B地”。非确定性的神奇之处在于,你可以想象自己分身成两个人,一个向左走,一个向右走,同时探索两条路。只要其中任何一个分身最终到达了目的地,就算你成功了。DPDA没有这个“分身”能力,它必须在岔路口做出唯一的选择,一旦选错就无法挽回。
💭 [直观想象]
- DPDA: 一个循规蹈矩的侦探。他严格按照线索A->B->C的顺序调查,如果线索中断,他就卡住了。
- NPDA: 一个可以“平行宇宙”思考的侦探。当线索A指向B和C两种可能时,他可以同时派出两个“意识分身”去调查B和C两条线。只要有一条线最终破案,整个案件就算成功。这就是NPDA能解决“猜中点”这类问题的原因。
📜 [原文10]
非确定性下推自动机识别某些语言,这是任何确定性下推自动机都无法识别的,我们将在 2.4 节中看到。我们在示例 2.16 和 2.18 中给出了需要非确定性的语言。回想一下,确定性和非确定性有限自动机确实识别同一类语言,因此下推自动机的情况是不同的。我们关注非确定性下推自动机,因为这些自动机在能力上与上下文无关文法等价。
📖 [逐步解释]
本段是对前一段观点的进一步阐述和总结,并为后续内容的安排提供了路线图。
- “非确定性下推自动机识别某些语言,这是任何确定性下推自动机都无法识别的,我们将在 2.4 节中看到”:
- 再次强调了NPDA比DPDA更强大。
- 并给出了一个明确的“预告”:关于这个主题的正式讨论和证明将在2.4节出现。这有助于管理读者的预期。
- 上下文无关语言的集合可以被划分为两部分:一部分是确定性上下文无关语言(可以被DPDA识别),另一部分是纯粹的非确定性上下文无关语言(只能被NPDA识别)。
- “我们在示例 2.16 和 2.18 中给出了需要非确定性的语言”:
- 这里再次预告,即将到来的两个例子(2.16和2.18)将会是展示非确定性必要性的具体案例。这让读者带着问题意识去阅读接下来的例子。
- “回想一下,确定性和非确定性有限自动机确实识别同一类语言,因此下推自动机的情况是不同的”:
- 通过再次与有限自动机进行对比,加深读者对PDA特殊性的印象。这种反复的对比是一种有效的教学方法。
- FA的世界:DFA 能力 = NFA 能力
- PDA的世界:DPDA 能力 < NPDA 能力
- “我们关注非确定性下推自动机,因为这些自动机在能力上与上下文无关文法等价”:
- 这解释了为什么本书(以及大多数计算理论教材)将重点放在NPDA上。
- 那个核心的、优美的等价定理(PDA 能力 = CFG 能力)是建立在非确定性模型之上的。DPDA只能对应上下文无关文法的一个子集(即确定性上下文无关文法),无法与整个CFG家族等价。
- 为了捕获所有上下文无关语言,我们必须使用非确定性的PDA。
💡 [数值示例]
- 示例1 (语言类别对比):
- 正则语言: $\{a,b\}$ 上所有偶数长度的字符串。可以被DFA识别,也可以被NFA识别。
- 确定性上下文无关语言: $\{0^n1^n \mid n \ge 0\}$。可以被DPDA识别,自然也可以被NPDA识别。
- 非确定性上下文无关语言: $\{ww^R \mid w \in \{0,1\}^*\}$。只能被NPDA识别,不能被任何DPDA识别。
- 示例2 (模型与语言类的关系):
- DFA/NFA <==> 正则文法 <==> 正则语言
- DPDA <==> 确定性上下文无关文法 <==> 确定性上下文无关语言
- NPDA <==> 上下文无关文法 (CFG) <==> 上下文无关语言
- 从这个关系中可以清晰地看到,DPDA只覆盖了上下文无关语言的一部分。
⚠️ [易错点]
- 易错点: 认为既然NPDA更强,DPDA就没用了。不是的,确定性模型在实际应用中(如编译器语法分析)非常重要,因为它们的行为是可预测的,效率更高,不会产生指数级的计算路径。很多编程语言的语法都被设计成确定性上下文无关语言,以便用高效的DPDA(或其变体)来解析。
📝 [总结]
本段明确指出,非确定性对于下推自动机至关重要,因为它赋予了PDA比其确定性版本更强的语言识别能力。这与有限自动机的情况不同。因此,为了与上下文无关文法建立完全的等价关系,我们将主要研究非确定性下推自动机(NPDA)。
🎯 [存在目的]
本段的目的是为课程的焦点选择提供一个明确的理由。它告诉读者:“我们接下来要花很多时间研究NPDA,而不是DPDA,原因在于NPDA才是与整个上下文无关语言世界完全对应的那个机器模型。” 这使得学习路径更加清晰和有目的性。
[直觉心-智模型]
想象你在画一幅画,有两种画笔:
- DPDA: 一支非常精密的工程绘图笔。它走的每一步都必须是精确的、预先计算好的。它能画出所有直线、圆和规则曲线构成的图形(确定性CFL)。
- NPDA: 一支神奇的“想象力画笔”。当你想画一个“梦境”时,你可以让画笔在某个点“发散”出多种可能性,探索不同的色彩和形状组合。最终只要有一种组合是你想要的,这幅画就成功了。这支笔不仅能画所有工程图形,还能画出那些模糊、重叠、充满“可能性”的艺术作品(非确定性CFL)。
为了研究所有可能的“画作”(CFL),我们必须研究这支神奇的“想象力画笔”(NPDA)。
💭 [直观想象]
- DPDA: 像在走一个设计好的迷宫,每个路口都只有一个正确的方向指示。
- NPDA: 像在玩一个角色扮演游戏,你在某个时刻可以做出不同的选择(“结盟”或“背叛”),每个选择都会开启一条全新的故事线。游戏接受你的存档,只要其中有一条故事线你能通关就行。
22. 下推自动机的形式定义
2.1. 下推自动机的形式定义
📜 [原文11]
下推自动机的形式定义类似于有限自动机,除了栈。栈是一种包含从某个字母表中提取的符号的设备。机器可以使用不同的字母表作为其输入字母表和栈字母表,因此现在我们指定输入字母表 $\Sigma$ 和栈字母表 $\Gamma$。
📖 [逐步解释]
本段开始进入下推自动机(PDA)的形式化定义部分,首先介绍了定义中新增的元素。
- “下推自动机的形式定义类似于有限自动机,除了栈”:
- 再次使用类比法。PDA的定义是一个六元组,而FA的定义是一个五元组。大部分组件是共享的或类似。
- FA: $(Q, \Sigma, \delta, q_0, F)$
- PDA将会增加与栈相关的组件。
- “栈是一种包含从某个字母表中提取的符号的设备”:
- 这定义了栈的内容。栈里面存放的不是任意的东西,而是一些预先定义好的符号。
- 这就引出了一个新的“字母表”概念,专门用于栈。
- “机器可以使用不同的字母表作为其输入字母表和栈字母表”:
- 这是一个重要的区分。
- 输入字母表 ($\Sigma$): 这是语言本身的符号集,是输入带上可能出现的符号。例如,对于 $\{0^n1^n\}$,$\Sigma = \{0, 1\}$。
- 栈字母表 ($\Gamma$): 这是可以存放在栈里的符号集。
- 这两个字母表可以是相同的,也可以是不同的,甚至可以有交集。通常,为了方便,我们会让 $\Sigma \subseteq \Gamma$,但这不是必须的。
- “因此现在我们指定输入字母表 $\Sigma$ 和栈字母表 $\Gamma$”:
- 在PDA的定义元组中,我们将需要明确地列出这两个字母表。
- $\Sigma$ (Sigma): 代表输入字母表。
- $\Gamma$ (Gamma): 代表栈字母表。
💡 [数值示例]
- 示例1: 识别 $\{0^n1^n \mid n \ge 0\}$
- 输入字母表: $\Sigma = \{0, 1\}$。因为输入字符串只包含 '0' 和 '1'。
- 栈字母表: $\Gamma$ 可以是 $\{X, \$\}$。
- 我们用 'X' 这个符号来代表我们看到了一个 '0'。
- 我们用 '$' 这个特殊的符号来标记栈的底部,这样我们就能知道栈什么时候变空了。
- 这里,$\Sigma$ 和 $\Gamma$ 是完全不同的集合。
- 示例2: 识别 $\{w\#w^R \mid w \in \{a,b\}^*\}$
- 输入字母表: $\Sigma = \{a, b, \#\}$。
- 栈字母表: $\Gamma = \{a, b, \$\}$。
- 我们直接把输入符号 'a' 和 'b' 压入栈中。
- 同样,'$' 用作栈底标记。
- 在这个例子中,$\{a,b\} \subset \Sigma$ 并且 $\{a,b\} \subset \Gamma$。$\Sigma$ 和 $\Gamma$ 有交集但并不相等。
⚠️ [易错点]
- 易错点: 忘记区分 $\Sigma$ 和 $\Gamma$。认为栈里只能存放输入符号。实际上,我们可以定义任意方便的符号作为栈符号,这能让PDA的设计更加清晰。例如,用一个栈符号 "A" 代表 "看到了三个连续的'b'",这都是允许的。
- 栈字母表 $\Gamma$ 必须是有限集。
📝 [总结]
本段为PDA的形式化定义做准备,指出了其定义与FA的相似性,并引入了PDA独有的一个新组件——栈字母表 $\Gamma$。它与输入字母表 $\Sigma$ 是两个独立的、需要被明确定义的符号集。
🎯 [存在目的]
本段的目的是在给出完整的六元组定义之前,先解释新增的组件。通过将复杂的定义分解,先介绍简单的部分($\Sigma$ 和 $\Gamma$),可以降低读者的认知负-担,使他们更容易理解接下来将要出现的高度复杂和核心的转移函数 $\delta$。
🧠 [直觉心智模型]
- 输入字母表 $\Sigma$: 你能“听懂”的语言词汇(比如英语单词)。
- 栈字母表 $\Gamma$: 你在记事本上写字时,可以使用的速记符号(比如用 -> 代表“导致”,用 ? 代表“疑问”)。
你可以用英语单词做速记,也可以用自己发明的、更高效的符号体系。这两个符号集是独立的。
💭 [直观想象]
- 输入带: 一本印着英文故事书。上面的文字来自输入字母表 $\Sigma = \{a, b, ..., z, \text{space}, ...\}$。
- 栈: 一叠便签纸。你可以在上面写任何你喜欢的符号,比如 ★, √, X。这些符号来自你的栈字母表 $\Gamma = \{★, √, X\}$。你可能会决定,每看到一个主人公的名字,就在便签上画一个 ★。
📜 [原文12]
任何自动机的形式定义的核心是转移函数,它描述了其行为。回想一下,$\Sigma_{\varepsilon}=\Sigma \cup\{\varepsilon\}$ 和 $\Gamma_{\varepsilon}=\Gamma \cup\{\varepsilon\}$。转移函数的域是 $Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon}$。因此,当前状态、读取的下一个输入符号和栈顶符号决定了下推自动机的下一个动作。任一符号都可以是 $\varepsilon$,导致机器在不读取输入符号或不读取栈符号的情况下移动。
📖 [逐步解释]
这一段聚焦于PDA形式定义中最复杂、最核心的部分——转移函数 $\delta$,详细解释了它的输入(定义域)。
- “任何自动机的形式定义的核心是转移函数,它描述了其行为”:
- 强调了转移函数 $\delta$ 的中心地位。它就是自动机的“程序”或“规则手册”,规定了自动机在所有可能情况下的所有合法动作。
- “回想一下,$\Sigma_{\varepsilon}=\Sigma \cup\{\varepsilon\}$ 和 $\Gamma_{\varepsilon}=\Gamma \cup\{\varepsilon\}$”:
- 这里引入了 $\varepsilon$-notation(epsilon notation)作为简写。
- $\Sigma_{\varepsilon}$: 表示输入字母表 $\Sigma$ 加上空字符串 $\varepsilon$。这意味着PDA的一次转移可以“不消耗”任何输入符号,即输入磁头不移动。这被称为 $\varepsilon$-转移。
- $\Gamma_{\varepsilon}$: 表示栈字母表 $\Gamma$ 加上 $\varepsilon$。这在转移函数的定义域和值域中都有特殊含义。
- 在定义域中(作为输入),栈顶符号 = ε 表示我们不关心栈顶是什么,或者说,这个转移不依赖于栈顶符号(在某些定义中,这可能意味着栈是空的)。更常见的解释是,这个转移不需要从栈中弹出任何符号。
- 在值域中(作为输出),压栈符号 = ε 意味着弹栈后不压入任何新符号,这实际上就是一个纯粹的弹栈(pop)操作。
- “转移函数的域是 $Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon}$”:
- 这句话用数学语言精确定义了PDA做决策所需要的所有信息。域(Domain)就是函数的输入。
- $Q$: 当前所在的状态。
- $\Sigma_{\varepsilon}$: 当前输入磁头指向的符号,或者是 $\varepsilon$(表示不读输入)。
- $\Gamma_{\varepsilon}$: 当前栈顶的符号,或者是 $\varepsilon$(表示不关心/不消耗栈顶符号)。
- 这个三元组 (state, input, stack_top) 完整地描述了PDA在任一时刻的“局面”(configuration)。
- “因此,当前状态、读取的下一个输入符号和栈顶符号决定了下推自动机的下一个动作”:
- “任一符号都可以是 $\varepsilon$,导致机器在不读取输入符号或不读取栈符号的情况下移动”:
- 强调了 $\varepsilon$ 的作用,它为PDA提供了巨大的灵活性。
- 不读取输入符号 ($\varepsilon$-move on input): 允许PDA在“原地”思考,只改变自己的状态和栈内容,而输入磁头不前进。这对于处理内部逻辑和准备后续步骤非常重要。
- 不读取栈符号 ($\varepsilon$-move on stack): 允许PDA在不消耗栈顶元素的情况下进行状态转移和压栈。这相当于一个纯粹的压栈操作,因为它没有弹出任何东西。
💡 [数值示例]
- 示例1 (普通转移):
- $\delta(q_1, '0', 'X') = \dots$
- 含义: 如果PDA当前在状态 $q_1$,输入磁头读到 '0',并且栈顶符号是 'X',那么就执行...
- 示例2 (输入为 $\varepsilon$):
- $\delta(q_2, \varepsilon, 'Y') = \dots$
- 含义: 如果PDA在状态 $q_2$,并且栈顶是 'Y',那么不需要读取任何输入,就可以执行...(输入磁头保持不动)。这常用于在读完一部分输入后,进行一些内部的栈清理或状态整理工作。
- 示例3 (栈顶为 $\varepsilon$):
- $\delta(q_3, '1', \varepsilon) = \dots$
- 含义: 如果PDA在状态 $q_3$,读到 '1',此时不关心栈顶是什么(或者说,不消耗栈顶符号),就执行... 这种转移通常用于压栈操作,因为它没有前提的弹栈。例如,push('Z') 可以表示为从栈顶 $\varepsilon$ 转移到压入 'Z'。
- 示例4 (输入和栈顶都为 $\varepsilon$):
- $\delta(q_4, \varepsilon, \varepsilon) = \dots$
- 含义: 在状态 $q_4$ 时,自动机可以自发地、无条件地进行一次状态或栈的改变,既不看输入,也不看栈顶。这是非确定性的一个强大来源。
⚠️ [易错点]
- 易错点: 对 $\varepsilon$ 在转移函数不同位置的含义感到困惑。
- 第二个位置 (输入): $\varepsilon$ 意味着“不读输入,磁头不动”。
- 第三个位置 (栈顶): $\varepsilon$ 意味着“不从栈上弹出任何符号”。
- 在函数的输出中,$\varepsilon$ 意味着“不向栈中压入任何符号”。
- 关键: 转移函数的输入是一个三元组 (状态, 输入, 弹栈),输出是一个二元组 (状态, 压栈)。$\varepsilon$ 的含义取决于它在哪个位置。
📝 [总结]
本段定义了PDA转移函数的输入部分(定义域),即 $Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon}$。它阐明了PDA的每一步决策都取决于当前状态、输入符号(或$\varepsilon$)和栈顶符号(或$\varepsilon$)这三个要素。$\varepsilon$的引入使得PDA可以在不消耗输入或栈内容的情况下进行转移,增加了其灵活性和非确定性。
🎯 [存在目的]
本段的目的是精确、无歧义地定义PDA的“决策依据”。在构建一个复杂的机器模型时,明确其在任意时刻的输入是什么至关重要。这是理解其行为和能力的基础,也是后续形式化定义和证明的关键。
🧠 [直觉心智模型]
想象一个需要多方信息才能做决策的经理。
- 当前状态 $Q$: 经理目前的情绪(乐观/悲观)。
- 输入符号 $\Sigma_{\varepsilon}$: 刚刚收到的新邮件内容(或者是“没有新邮件” $\varepsilon$)。
- 栈顶符号 $\Gamma_{\varepsilon}$: 他办公桌上最顶层的文件是什么(或者是“桌上没文件” $\varepsilon$)。
他的下一个行动(决定进入哪个新情绪状态,以及在桌上放什么新文件)是这三者共同作用的结果。
💭 [直观想象]
一个国际象棋AI:
- 当前状态 $Q$: 可能是AI的评估模式(“进攻”、“防守”)。
- 输入符号 $\Sigma_{\varepsilon}$: 对手的最新一步棋。
- 栈顶符号 $\Gamma_{\varepsilon}$: AI的“思考栈”中正在考虑的最优先的候选招法。
AI的下一步棋,取决于它当前的策略、对手的行动以及它自己正在计算的招法。它甚至可以在对手没走棋的情况下(输入为 $\varepsilon$)继续深化自己的思考(进行内部状态和栈的调整)。
📜 [原文13]
对于转移函数的范围,我们需要考虑当自动机处于特定情况时允许它做什么。它可能会进入某个新状态,并可能在栈顶写入一个符号。函数 $\delta$ 可以通过返回 $Q$ 的一个成员和 $\Gamma_{\varepsilon}$ 的一个成员来指示此操作,即 $Q \times \Gamma_{\varepsilon}$ 的一个成员。因为我们允许此模型中的非确定性,所以一种情况可能有几个合法的下一步动作。转移函数以通常的方式结合了非确定性,通过返回 $Q \times \Gamma_{\varepsilon}$ 的成员集,即 $\mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$ 的成员。总而言之,我们的转移函数 $\delta$ 的形式为 $\delta: Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon} \longrightarrow \mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$。
📖 [逐步解释]
这一段接着上一段,定义了PDA转移函数 $\delta$ 的输出(值域或范围),并正式引入了非确定性。
- “对于转移函数的范围,我们需要考虑当自动机处于特定情况时允许它做什么”:
- 范围 (Range) 或 值域 (Codomain) 指的是函数的输出集合。上一段定义了输入 (状态, 输入, 栈顶),这一段定义了输出是什么。
- 自动机的动作包括两个方面:改变状态和改变栈。
- “它可能会进入某个新状态,并可能在栈顶写入一个符号”:
- 进入某个新状态: 输出的一部分是 $Q$ 中的一个元素,即下一个状态。
- 在栈顶写入一个符号: 输出的另一部分是 $\Gamma$ 中的一个元素,即要压入栈的符号。
- “可能” 这个词很重要。如果写入的是 $\varepsilon$ 呢?那就意味着不写入任何符号,这对应于一个纯粹的弹栈操作。所以输出的栈符号来自 $\Gamma_{\varepsilon} = \Gamma \cup \{\varepsilon\}$。
- “函数 $\delta$ 可以通过返回 $Q$ 的一个成员和 $\Gamma_{\varepsilon}$ 的一个成员来指示此操作,即 $Q \times \Gamma_{\varepsilon}$ 的一个成员”:
- 这给出了一个确定性动作的输出形式:一个序对 (next_state, symbol_to_push)。
- 这个序对属于笛卡尔积 $Q \times \Gamma_{\varepsilon}$。
- “因为我们允许此模型中的非确定性,所以一种情况可能有几个合法的下一步动作”:
- 这是关键的一步,引入了非确定性。
- 对于同一个输入三元组 (q, a, x),PDA 可能有多个选择。比如,既可以转移到 (q1, Y),也可以转移到 (q2, Z)。
- “转移函数以通常的方式结合了非确定性,通过返回 $Q \times \Gamma_{\varepsilon}$ 的成员集,即 $\mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$ 的成员”:
- 这里解释了如何在数学上表示非确定性。我们不返回单个序对,而是返回一个包含所有可能序对的集合。
- $\mathcal{P}(S)$ 是集合 $S$ 的幂集(Power Set),代表 $S$ 的所有子集构成的集合。
- 因此,$\delta$ 的输出是 $\mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$ 中的一个元素,也就是一个形如 {(q1, Y), (q2, Z), ...} 的集合。
- 如果某个转移是确定性的,那么这个返回的集合里就只有一个元素。如果某个转移是不允许的,就返回空集 $\emptyset$。
- “总而言之,我们的转移函数 $\delta$ 的形式为 $\delta: Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon} \longrightarrow \mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$”:
- 这是对PDA转移函数的最终、完整的形式化定义。
- $\delta$: 转移函数的名称。
- :: 读作 "maps from" (从...映射)。
- $Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon}$: 定义域(输入),一个三元组 (当前状态, 当前输入或ε, 栈顶符号或ε)。
- $\longrightarrow$: 映射箭头。
- $\mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$: 值域(输出),一个可能的新 (状态, 压栈符号或ε) 序对的集合。
💡 [数值示例]
假设一个PDA有状态 $\{q_1, q_2\}$,输入字母表 $\{0, 1\}$,栈字母表 $\{X\}$。
$\delta(q_1, '0', \varepsilon) = \{ (q_1, X) \}$
- 解释: 在状态 $q_1$,读到 '0',不消耗栈内容时,唯一的动作是:保持在状态 $q_1$,并压入 'X'。这是一个典型的压栈操作。
$\delta(q_1, \varepsilon, X) = \{ (q_1, \varepsilon), (q_2, \varepsilon) \}$
- 解释: 在状态 $q_1$,不读输入,且栈顶是 'X' 时,有两个选择:
- (q1, ε): 保持在状态 $q_1$,并弹出 'X' (因为压入的是 $\varepsilon$)。
- (q2, ε): 转移到状态 $q_2$,并弹出 'X'。
$\delta(q_2, '0', X) = \emptyset$
- 解释: 在状态 $q_2$,如果读到 '0' 且栈顶是 'X',则没有合法的转移。这条计算路径会“死亡”。
⚠️ [易错点]
- 易错点: 将 $\rightarrow$ 与 $\longrightarrow$ 混淆。在文法中,$\rightarrow$ 表示“生成”。在这里,$\longrightarrow$ 是标准的函数映射符号。
- 关键理解: $\delta$ 的输出是一个集合,这正是非确定性的数学体现。如果PDA是确定性的,那么这个输出集合的大小永远是0或1。
📝 [总结]
本段完整地定义了PDA的转移函数 $\delta: Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon} \longrightarrow \mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$。它明确了PDA的“动作”是产生一个新状态和压入一个栈符号(或$\varepsilon$),并且由于非确定性,对于一个给定的“局面”,可能会有一集合的合法动作可供选择。
🎯 [存在目的]
本段的目的是为PDA的行为提供一个严格、无歧义的数学框架。这个转移函数的定义是整个PDA模型的心脏,后续所有的计算、接受、拒绝等概念都将基于这个函数来定义。精确地理解 $\delta$ 的输入和输出是掌握PDA的关键。
🧠 [直觉心智模型]
转移函数 $\delta$ 就是一本“命运之书”。
- 输入 (状态, 输入, 栈顶): 书的“索引”,描述了你当前所处的“时空坐标”。
- 输出 {(新状态, 压栈), ...}: 翻到索引对应的那一页,上面写着你所有可能的“未来”。可能只有一种未来(确定性),也可能有多条平行的未来路径供你选择(非确定性),或者这一页是空白的,意味着你走入了死胡同($\emptyset$)。
💭 [直观想象]
想象你在下一个象棋的指令。
- 指令的输入 (Domain): (当前棋子布局, 对手刚走的棋, 你正在考虑的棋)
- 指令的输出 (Range): { (走法A后的新棋局), (走法B后的新棋局), ... }
这个指令集就是转移函数。它告诉你,在当前情况下,有哪些合法的走法,以及每种走法会导致什么样的新局面。对于象棋大师来说,一个局面下往往有多个不错的候选走法,这就是一种非确定性。
2.2. 定义 2.13
📜 [原文14]
下推自动机是一个 6 元组 ( $Q, \Sigma, \Gamma, \delta, q_{0}, F$ ),其中 $Q, \Sigma$,$\Gamma$ 和 $F$ 都是有限集,并且
- $Q$ 是状态集,
- $\Sigma$ 是输入字母表,
- $\Gamma$ 是栈字母表,
- $\delta: Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon} \longrightarrow \mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$ 是转移函数,
- $q_{0} \in Q$ 是开始状态,并且
- $F \subseteq Q$ 是接受状态集。
📖 [逐步解释]
这是下推自动机(PDA)的完整、形式化的定义。它将PDA定义为一个包含六个部分的数学对象(一个6元组)。
- “下推自动机是一个 6 元组 (...)”:
- 元组 (Tuple) 是一种包含了固定数量元素的数学结构。说PDA是6元组,意味着要完整地描述一个PDA,我们必须且只需指定这六个组件。
- 对比:有限自动机(DFA/NFA)是5元组 $(Q, \Sigma, \delta, q_0, F)$。PDA多了一个组件 $\Gamma$(栈字母表),并且转移函数 $\delta$ 的形式也更复杂。
- “$Q, \Sigma, \Gamma$ 和 $F$ 都是有限集”:
- 这是一个重要的约束。
- $Q$ (状态集) 是有限的,这使得控制部分是“有限控制”。
- $\Sigma$ (输入字母表) 是有限的。
- $\Gamma$ (栈字母表) 也是有限的。虽然栈的深度是无限的,但可以往里面放的符号种类是有限的。
- $F$ (接受状态集) 是有限的,它是 $Q$ 的一个子集。
- 逐项解释六元组:
- 1. $Q$ 是状态集:
- 和FA一样,代表自动机所有可能的内部状态。例如,{q_start, q_reading_zeros, q_reading_ones, q_accept}。
- 2. $\Sigma$ 是输入字母表:
- 定义了哪些符号可以出现在输入带上。例如,{0, 1}。
- 3. $\Gamma$ 是栈字母表:
- 这是PDA新增的组件。定义了哪些符号可以被压入栈中。例如,{Z, $}`,其中 Z 用来计数,`$ 用作栈底标记。
- 4. $\delta: Q \times \Sigma_{\varepsilon} \times \Gamma_{\varepsilon} \longrightarrow \mathcal{P}\left(Q \times \Gamma_{\varepsilon}\right)$ 是转移函数:
- 这是PDA的“大脑”,定义了它的全部行为,正如前文所详细解释的。它根据 (当前状态, 输入符号, 栈顶符号) 决定一个或多个 (新状态, 压栈符号) 的动作。
- 5. $q_{0} \in Q$ 是开始状态:
- 和FA一样,这是自动机启动时所处的唯一状态。计算总是从 $q_0$ 开始。
- 6. $F \subseteq Q$ 是接受状态集:
- 和FA一样,这是一个状态的子集。如果自动机在读取完所有输入后,它的某条计算路径停在了 $F$ 中的某个状态,那么该输入字符串就被接受。
💡 [数值示例]
- 示例: 一个(不完整的)PDA M 的定义
- $Q = \{q_0, q_1, q_f\}$
- $\Sigma = \{a, b\}$
- $\Gamma = \{X, \$\}$
- $\delta$: (这里只写几个转移规则作为示例)
- $\delta(q_0, a, \$) = \{(q_0, X\$)\}$ (读'a',栈底是'$',则**压入**'X',保持**状态** $q_0$)
- $\delta(q_0, b, X) = \{(q_1, \varepsilon)\}$ (读'b',栈顶是'X',则弹出'X',进入状态 $q_1$)
- $\delta(q_1, \varepsilon, \$) = \{(q_f, \$)\}$ (在 $q_1$ 不读输入,看到栈底,则进入 $q_f$)
- $q_0$ 就是 $q_0$
- $F = \{q_f\}$
这个例子给出了一个PDA所需的所有六个组件的具体实例。
⚠️ [易错点]
- 易错点:
- 忘记 $\Gamma$。PDA比FA多一个字母表。
- 对 $\delta$ 的形式记忆不清。必须记住它的输入是三元组,输出是二元组的集合。
- 与FA的对比:
- FA五元组: $(Q, \Sigma, \delta_{FA}, q_0, F)$,其中 $\delta_{FA}: Q \times \Sigma \to Q$ (DFA) 或 $Q \times \Sigma_{\varepsilon} \to \mathcal{P}(Q)$ (NFA)。
- PDA六元组: $(Q, \Sigma, \Gamma, \delta_{PDA}, q_0, F)$,其中 $\delta_{PDA}$ 形式复杂得多。
📝 [总结]
本段给出了下推自动机(PDA)的严格数学定义,即一个六元组 $(Q, \Sigma, \Gamma, \delta, q_0, F)$。它由状态集 $Q$、输入字母表 $\Sigma$、栈字母表 $\Gamma$、转移函数 $\delta$、开始状态 $q_0$ 和接受状态集 $F$ 构成。其中,$\Gamma$ 和 $\delta$ 的复杂形式是其与有限自动机的核心区别。
🎯 [存在目的]
本段的目的是提供一个坚实的、无歧义的理论基础。形式化定义是所有后续证明和算法描述的基石。有了这个定义,我们就可以精确地讨论PDA的计算过程、接受条件以及它的能力范围,而不会产生误解。这是从非正式描述到严格科学论证的必要一步。
🧠 [直觉心智模型]
定义一个PDA就像是在为一个游戏编写规则书。
- $Q$: 游戏所有可能的“关卡”。
- $\Sigma$: 玩家可以输入的指令类型(如“上、下、左、右”)。
- $\Gamma$: 玩家背包里可以放的道具种类(如“钥匙、药水”)。
- $\delta$: 规则书的核心章节,详细说明了“在哪个关卡,输入什么指令,且背包顶部有什么道具时,你会进入哪个新关卡,并且会往背包里放什么新道具”。
- $q_0$: 游戏的“第一关”。
- $F$: 所有“通关”关卡的集合。
💭 [直观想象]
这就像在组装一台机器。
- $Q$: 一盒不同功能的齿轮。
- $\Sigma$: 机器能处理的原材料类型。
- $\Gamma$: 机器内部临时存储仓可以存放的零件类型。
- $\delta$: 详细的组装蓝图,告诉你每个齿轮如何与输入和存储仓连接。
- $q_0$: 机器的启动齿轮。
- $F$: 能让机器亮起“完成”绿灯的那些齿轮。
只有集齐这六样东西,并正确组装,你才能得到一台功能完整的下推自动机。
📜 [原文15]
下推自动机 $M=\left(Q, \Sigma, \Gamma, \delta, q_{0}, F\right)$ 的计算方式如下。如果 $w$ 可以写成 $w=w_{1} w_{2} \cdots w_{m}$,其中每个 $w_{i} \in \Sigma_{\varepsilon}$,并且存在状态序列 $r_{0}, r_{1}, \ldots, r_{m} \in Q$ 和字符串 $s_{0}, s_{1}, \ldots, s_{m} \in \Gamma^{*}$ 满足以下三个条件,则它接受输入 $w$。字符串 $s_{i}$ 表示 $M$ 在计算的接受分支上具有的栈内容序列。
📖 [逐步解释]
在定义了PDA的静态结构之后,这一段开始定义它的动态行为——计算过程,以及什么是“接受”一个输入字符串。
- “下推自动机 M 的计算方式如下”:
- 这部分定义了PDA如何处理一个输入字符串 $w$。
- “如果 $w$ 可以写成 $w=w_{1} w_{2} \cdots w_{m}$,其中每个 $w_{i} \in \Sigma_{\varepsilon}$”:
- 这里对输入字符串 $w$ 进行了分解。重要的是,$w_i$ 可以是 $\Sigma$ 中的一个符号,也可以是空字符串 $\varepsilon$。
- 这意味着PDA的计算过程被看作是一系列离散的“步骤”,总共 $m$ 步。在每一步 $i$,PDA消耗输入 $w_i$。如果 $w_i = \varepsilon$,则这一步是 $\varepsilon$-转移,输入磁头不移动。
- “并且存在状态序列 $r_{0}, r_{1}, \ldots, r_{m} \in Q$ 和字符串 $s_{0}, s_{1}, \ldots, s_{m} \in \Gamma^{*}$”:
- 这里引入了两个与计算步骤相对应的序列:
- 状态序列 $r_0, r_1, \dots, r_m$: $r_i$ 是PDA在第 $i$ 步计算之后达到的状态。
- 栈内容序列 $s_0, s_1, \dots, s_m$: $s_i$ 是一个字符串,代表PDA在第 $i$ 步计算之后,整个栈的内容(从栈顶到栈底)。$\Gamma^*$ 表示由栈字母表中的符号组成的所有可能字符串。
- “满足以下三个条件,则它接受输入 $w$”:
- “接受”的定义是:必须能找到这样一组序列($w_i, r_i, s_i$),它们如同一个合法的计算日志,记录了从开始到结束的全过程,并满足下面三个条件。由于非确定性,可能存在很多不同的计算日志,只要其中至少有一条满足条件,就算接受。
- “字符串 $s_{i}$ 表示 $M$ 在计算的接受分支上具有的栈内容序列”:
- 这句话 уточняет, что мы ищем одну конкретную, "успешную" ветку вычислений среди всех возможных, порожденных недетерминизмом.
💡 [数值示例]
- 示例: 假设 $w = ab$。
- 一种可能的分解是 $w_1=a, w_2=b$。那么 $m=2$。我们需要找到序列:
- $r_0, r_1, r_2$ (状态)
- $s_0, s_1, s_2$ (栈内容)
- 另一种可能的分解是 $w_1=a, w_2=\varepsilon, w_3=b$。那么 $m=3$。我们需要找到序列:
- $r_0, r_1, r_2, r_3$
- $s_0, s_1, s_2, s_3$
- 这种分解方式的灵活性,正是为了容纳 $\varepsilon$-转移。
⚠️ [易错点]
- 易错点: 认为 $m$ 必须等于输入字符串 $w$ 的长度。不一定,因为 $\varepsilon$-转移的存在,$m$ 可以大于等于 $w$ 的长度。
- 关键: “存在” (there exist) 这个词是非确定性的核心。我们不需要检查所有可能的计算路径。我们只需要找到一条“黄金路径”即可证明接受。如果找不到任何一条这样的路径,则拒绝。
📝 [总结]
本段为“接受”一个输入字符串 $w$ 设定了框架。它指出,接受 $w$ 意味着存在一个合法的计算序列,该序列由输入的分解、状态的演变和栈内容的演变共同构成,并且这个序列必须满足接下来将要陈述的三个条件。
🎯 [存在目的]
本段的目的是将PDA的静态定义(六元组)和它的动态行为(计算)联系起来。它建立了一个数学框架来描述“一步计算”和“整个计算历史”,为接下来定义合法计算的三个核心规则铺平了道路。
🧠 [直觉心智模型]
这就像是在法庭上证明一个嫌疑人(输入字符串 $w$)是“清白的”(被接受)。
- 你(PDA的证明者)不需要展示嫌疑人每天的每一分钟都在做什么。
- 你只需要提供一个“不在场证明”的时间线(一个合法的计算序列 $w_i, r_i, s_i$)。
- 这个时间线必须是连贯的、合法的,并最终导向一个“清白”的结局(满足三个条件)。
- 只要你能提供这样一条时间线,法官(理论家)就判定嫌疑人“清白”。至于嫌疑人是否还有其他可疑的时间线,在非确定性模型下我们不关心。
💭 [直观想象]
这就像在玩一个解谜游戏,要从起点走到终点。
- 输入 $w$: 谜题的全部线索。
- $w_i$序列: 你决定如何使用这些线索的顺序(你可以一个一个用,也可以在原地“思考”一步,即使用 $\varepsilon$)。
- $r_i$序列: 你在解谜过程中所处的每一个房间(状态)。
- $s_i$序列: 你每到一个新房间时,你背包里的道具(栈内容)是什么样的。
要证明你能解开这个谜题,你只需要展示一条完整的、从起点到终点的路径,并说明你在每个房间、使用每个线索时,背包里的道具是如何变化的,而且每一步都符合游戏规则。
📜 [原文16]
- $r_{0}=q_{0}$ 且 $s_{0}=\varepsilon$。此条件表示 $M$ 以开始状态和空栈正确启动。
- 对于 $i=0, \ldots, m-1$,我们有 $\left(r_{i+1}, b\right) \in \delta\left(r_{i}, w_{i+1}, a\right)$,其中 $s_{i}=a t$ 和 $s_{i+1}=b t$ 对于某些 $a, b \in \Gamma_{\varepsilon}$ 和 $t \in \Gamma^{*}$。此条件表示 $M$ 根据状态、栈和下一个输入符号正确移动。
- $r_{m} \in F$。此条件表示在输入结束时出现接受状态。
📖 [逐步解释]
这是PDA接受一个字符串所需满足的三个核心条件,它们定义了什么是“合法的计算历史”。
条件1: $r_{0}=q_{0}$ 且 $s_{0}=\varepsilon$。
- $r_0 = q_0$: 计算序列的第一个状态 $r_0$ 必须是PDA定义的开始状态 $q_0$。这意味着计算必须从指定的起点开始。
- $s_0 = \varepsilon$: 计算序列的第一个栈内容 $s_0$ 必须是空字符串 $\varepsilon$。这意味着计算开始时,栈必须是空的。
- “此条件表示 M 以开始状态和空栈正确启动”: 这是对条件的清晰解释。它定义了计算的“初始配置”。
条件2: $\left(r_{i+1}, b\right) \in \delta\left(r_{i}, w_{i+1}, a\right)$,其中 $s_{i}=a t$ 和 $s_{i+1}=b t$ ...
- 这是最核心、最复杂的条件,它定义了计算的“转移合法性”。它描述了PDA如何从第 $i$ 步的配置 (r_i, s_i) 移动到第 $i+1$ 步的配置 (r_{i+1}, s_{i+1})。
- $s_i = at$:
- $s_i$ 是第 $i$ 步的栈内容。
- $a \in \Gamma_{\varepsilon}$ 是当时的栈顶符号。如果 $a=\varepsilon$,意味着我们不从栈中弹出任何东西。
- $t \in \Gamma^*$ 是栈中余下的部分(t for tail)。
- $s_{i+1} = bt$:
- $s_{i+1}$ 是第 $i+1$ 步的新栈内容。
- $b \in \Gamma_{\varepsilon}$ 是转移后新压入的符号。如果 $b=\varepsilon$,意味着我们不压入任何新东西。
- 关键: t 部分保持不变!这精确地模拟了栈操作:只在栈顶进行,栈的深层部分 t 不受影响。
- a 被 b 替换了。
- push Y: $a=\varepsilon, b=Y$。$s_i = t, s_{i+1} = Yt$。
- pop X: $a=X, b=\varepsilon$。$s_i = Xt, s_{i+1} = t$。
- replace X with Y: $a=X, b=Y$。$s_i = Xt, s_{i+1} = Yt$。
- $(r_{i+1}, b) \in \delta(r_i, w_{i+1}, a)$:
- 这句是说,我们观察到的状态变化(从 $r_i$ 到 $r_{i+1}$)和栈顶变化(从 $a$ 变为 $b$),必须是转移函数 $\delta$ 在给定当前状态 $r_i$ 和输入 $w_{i+1}$ 的情况下,所允许的一个合法操作。
- “此条件表示 M 根据状态、栈和下一个输入符号正确移动”: 这是对条件的完美总结。它保证了计算的每一步都遵循了PDA的规则手册 $\delta$。
条件3: $r_{m} \in F$。
- $r_m$: 这是计算序列的最后一个状态,也就是在处理完所有输入(包括所有$\varepsilon$-转移)后,PDA所处的状态。
- $F$: 接受状态集。
- $r_m \in F$: 最后一个状态必须是接受状态之一。
- “此条件表示在输入结束时出现接受状态”: 这是计算成功的最终判定标准。
💡 [数值示例]
- 示例: 一个简单的PDA,$\delta(q_0, 'a', \varepsilon) = \{(q_1, 'X')\}$ 且 $\delta(q_1, 'b', 'X') = \{(q_f, \varepsilon)\}$。$q_0$是开始状态,$q_f$是接受状态。我们来验证输入 $w="ab"$ 是否被接受。
- 令 $w_1='a', w_2='b'$。所以 $m=2$。
- 我们需要找到 $r_0, r_1, r_2$ 和 $s_0, s_1, s_2$ 满足所有条件。
- 步骤0 (初始配置):
- 根据条件1: $r_0 = q_0$, $s_0 = \varepsilon$。
- 步骤1 (处理 $w_1='a'$):
- 当前配置: $(r_0, w_1, s_0) = (q_0, 'a', \varepsilon)$。
- 栈分解: $s_0 = at$ -> $\varepsilon = \varepsilon \cdot \varepsilon$。所以 $a=\varepsilon, t=\varepsilon$。
- 查找转移函数: $\delta(q_0, 'a', \varepsilon) = \{(q_1, 'X')\}$。
- 这个转移是合法的!我们可以选择 (q_1, 'X')。
- 所以, $r_1=q_1, b='X'$。
- 新栈: $s_1 = bt = 'X' \cdot \varepsilon = 'X'$。
- 条件2在 $i=0$ 时满足。
- 步骤2 (处理 $w_2='b'$):
- 当前配置: $(r_1, w_2, s_1) = (q_1, 'b', 'X')$。
- 栈分解: $s_1 = at$ -> $'X' = 'X' \cdot \varepsilon$。所以 $a='X', t=\varepsilon$。
- 查找转移函数: $\delta(q_1, 'b', 'X') = \{(q_f, \varepsilon)\}$。
- 这个转移是合法的!我们可以选择 (q_f, ε)。
- 所以, $r_2=q_f, b=\varepsilon$。
- 新栈: $s_2 = bt = \varepsilon \cdot \varepsilon = \varepsilon$。
- 条件2在 $i=1$ 时满足。
- 步骤3 (最终检查):
- 计算结束。$m=2$。
- 最后一个状态是 $r_2 = q_f$。
- 检查条件3: $r_2 \in F$?因为 $F=\{q_f\}$,所以 $q_f \in F$ 成立。
- 结论: 我们成功地找到了一个满足所有三个条件的计算序列。因此,字符串 "ab" 被此PDA接受。
⚠️ [易错点]
- 易错点:
- 对条件2中 $s_i=at, s_{i+1}=bt$ 的理解。一定要记住 t 是不变的,它代表了栈操作的本质。
- 忘记非确定性。$\delta$ 的输出是一个集合,我们只需要从集合中找到一个能让计算继续下去的 (r_{i+1}, b) 即可。
- 接受的两种方式: 这里定义的是“通过接受状态接受”。还有另一种等价的定义方式,叫“通过空栈接受”,即计算结束时栈必须为空,而不需要关心最后一个状态是不是接受状态。本书采用的是“通过接受状态接受”的定义。
📝 [总结]
这三条规则形式化地定义了PDA的计算和接受过程:
- 启动规则: 计算必须从开始状态和空栈开始。
- 转移规则: 计算的每一步都必须是转移函数 $\delta$ 所允许的合法操作。
- 接受规则: 整个输入字符串处理完毕后,自动机必须停在一个接受状态。
只有当存在一条计算路径同时满足这三个条件时,输入字符串才被接受。
🎯 [存在目的]
这三个条件是PDA理论的基石。它们将PDA的静态定义(六元组)转化为一个能动的、可执行的计算模型。任何关于PDA能做什么、不能做什么的证明,都最终要回归到这三个基本条件的检验上。它们为PDA的行为提供了最终的、无歧义的仲裁标准。
🧠 [直觉心智模型]
这就像在验证一个程序的执行日志是否合法。
- 条件1: 日志的第一行必须是“程序在main函数入口启动,内存为空”。
- 条件2: 日志中的每一行到下一行的变化,都必须是程序代码所允许的一步合法操作。你不能凭空变出一个变量的值。
- 条件3: 日志的最后一行必须是“程序正常退出,返回码为0”。
如果能找到这样一份“完美”的日志,就说明程序可以在该输入下成功运行。
💭 [直观想象]
这就像在审核一份成功的太空任务报告。
- 条件1: 报告必须从“火箭在肯尼迪航天中心发射台,燃料箱为空(准备加注)”开始。
- 条件2: 报告的每一个阶段,从“发射”到“进入轨道”到“月球着陆”,都必须遵循牛顿定律和飞行控制手册($\delta$)里的规定。飞船的位置、速度、燃料剩余量(栈)的变化必须是合法的。
- 条件3: 报告的最后一页必须是“宇航员成功登陆月球,插上旗帜(进入接受状态)”。
只有这样一份完整的、每一步都合法的、并最终成功的报告存在,我们才能说这次任务(对输入字符串的计算)是成功的(被接受的)。
33. 下推自动机的例子
3.1. 例子 2.14
📜 [原文17]
以下是识别语言 $\left\{0^{n} 1^{n} \mid n \geq 0\right\}$ 的 PDA(第 112 页)的形式描述。设 $M_{1}$ 是 ( $Q, \Sigma, \Gamma, \delta, q_{1}, F$ ),其中
$$
\begin{aligned}
& Q=\left\{q_{1}, q_{2}, q_{3}, q_{4}\right\}, \\
& \Sigma=\{0,1\}, \\
& \Gamma=\{0, \$\}, \\
& F=\left\{q_{1}, q_{4}\right\}, \text { 并且 }
\end{aligned}
$$
$\delta$ 由下表给出,其中空白条目表示 $\emptyset$。
| 输入: 栈: |
0 |
|
|
1 |
|
|
$\varepsilon$ |
|
|
|
0 |
\$ | $\varepsilon$ | 0 | \$ |
$\varepsilon$ |
0 |
\$ | $\varepsilon$ |
| $q_{1}$ |
|
|
$\left\{\left(q_{2}, \$\right)\right\}$ |
|
|
|
|
|
|
| $q_{2}$ |
$\left\{\left(q_{2}, 0\right)\right\}$ |
|
|
$\left\{\left(q_{3}, \boldsymbol{\varepsilon}\right)\right\}$ |
|
|
|
|
|
| $q_{3}$ |
|
|
|
$\left\{\left(q_{3}, \varepsilon\right)\right\}$ |
|
|
|
$\left\{\left(q_{4}, \boldsymbol{\varepsilon}\right)\right\}$ |
|
| $q_{4}$ |
|
|
|
|
|
|
|
|
|
📖 [逐步解释]
这个例子给出了识别经典非正则语言 $\{0^n1^n \mid n \ge 0\}$ 的一个具体的、形式化的PDA定义。
- 六元组的定义:
- $Q=\left\{q_{1}, q_{2}, q_{3}, q_{4}\right\}$: 状态集包含四个状态。我们可以非正式地理解它们的作用:
- $q_1$: 开始状态,准备开始处理字符串。
- $q_2$: 正在读取和压栈 '0' 的阶段。
- $q_3$: 正在读取和弹栈 '1' 的阶段。
- $q_4$: 完成匹配,到达最终接受状态。
- $\Sigma=\{0,1\}$: 输入字母表,很标准。
- $\Gamma=\{0, \$\}$**: **栈字母表**。这里作者选择用**输入符号** '0' 直接作为**栈符号**来计数。'$' 是一个特殊的栈底**标记。
- $q_1$: 开始状态被指定为 $q_1$。
- $F=\left\{q_{1}, q_{4}\right\}$: 接受状态集包含两个状态。
- $q_1 \in F$: 意味着如果输入是空字符串 $\varepsilon$ ($n=0$ 的情况),自动机会停留在 $q_1$ 并直接接受。
- $q_4 \in F$: 意味着对于 $n>0$ 的情况,成功匹配后会到达 $q_4$ 并接受。
- $\delta$ (转移函数) 表格的解读:
- 表格结构:
- 行: 代表当前状态 ($q_1, q_2, q_3, q_4$)。
- 列: 代表输入符号 (0, 1, $\varepsilon$) 和栈顶符号 (0, $, \varepsilon$) 的组合。这是一个二维化的表示,更易读。例如,第一大列 "0" 下面有三小列,分别对应栈顶是 '0', '$', '$\varepsilon$' 的情况。
- 解读每一个非空条目:
- **$\delta(q_1, \varepsilon, \varepsilon) = \{(q_2, \$)\}$** (原文表格中 $q_1$行, $\varepsilon$输入, $\varepsilon$栈顶列):
- 含义: 在开始状态 $q_1$,不读任何输入,也不看栈顶(因为栈是空的),自动机可以进行一次 $\varepsilon$-转移。
- 动作: 转移到状态 $q_2$,并压入 '$' 符号。
- 目的: 这是初始化步骤。在开始处理0和1之前,先把栈底标记 '$' 放入**栈**中。然后进入处理0的**状态** $q_2$。
- 注意: 表格的q1行, 0输入, ε栈顶是 `{(q2, $)}`。这是原文的笔误或一种不寻常的表示法。从逻辑上看,这应该是 $\delta(q_1, 0, \varepsilon)$ 或者 $\delta(q_1, \varepsilon, \varepsilon)$。从后面的状态图来看,它应该是 $\varepsilon, \varepsilon \to \
$$
之后再处理0。让我们按照逻辑来分析,假设在进入 $q_2$ 之后才开始读 '0'。更合理的解释是,表格第一行 `q1` 下的 `{(q2, $)}` 实际上是 $\delta(q_1, 0, \varepsilon)$,意味着读第一个0的时候顺便把栈底符号压进去。但通常是先压栈底。我们先按表格字面意思理解,但后面会看到状态图给出了更清晰的逻辑。**更正一下**,仔细看表格的列标题,第一大列是输入0,下面三小列是栈顶0, $, ε。所以q1行那个条目是 $\delta(q_1, 0, \varepsilon) = \{(q_2, 0\$)\}$,但它只写了 `{(q2, $)}`,可能省略了0。而状态图画的是 $\varepsilon,\varepsilon\to\
$$
,这与表格不完全对应。我们以表格为准,并对其进行最合理的解释。让我们重新审视表格,它可能是一种简写:
- $\delta(q_1, \varepsilon, \varepsilon) = \{(q_2, \$)\}$: 这似乎是 (q1, ε, ε) 处的转移。
- 但表格把它放在了 (q1, 0, ε) 下面。这很奇怪。让我们参考后面的状态图和常规做法:PDA通常会先做一个 $\varepsilon$-转移把 '$' **压栈**,然后进入一个新**状态**。或者在读第一个0的时候一起**压**。我们假设表格的意图是 `$\delta(q_1, \varepsilon, \varepsilon) = \{(q_2, \$)\}$。但是如果严格按照表格, $\delta(q_1, 0, \varepsilon) = \{(q_2, \$)\}$`,这表示读第一个0,然后把$压栈,这个0本身没有被压栈,这逻辑不通。所以我们采纳更合理的解释:有一个从 $q_1$ 到 $q_2$ 的 $\varepsilon, \varepsilon \to \$$ 转移,或者表格的 $q_2$ 行的 $\delta(q_2, 0, 0)$ 应该在 $q_1$ 状态就开始。
- 让我们采用一个更符合逻辑的解读,这个PDA的设计可能如下: $q_1$是接受空字符串的。要处理非空字符串,它先`ε,ε -> $`转移到另一个状态(比如$q_{start'}$),然后从$q_{start'}$开始读0。这个表格可能简化了这个过程。
- 最可能的解释: 表格的q1行 ε列下是空的, 0列ε栈顶下是 `{(q2, $)}`。这可能是指 $\delta(q_1, 0, \varepsilon)$,但是这样的话第一个0没有被计数。这与算法描述不符。
- 让我们看 $q_2$ 的转移,这更清晰:
- $\delta(q_2, 0, 0) = \{(q_2, 00)\}$: 这是笔误。应该是 {(q2, 0)},即压入一个0。逻辑是:在 $q_2$ 状态(读0阶段),如果输入是'0',栈顶也是'0',那么就再压入一个'0'。这样栈就变成 00...。
- $\delta(q_2, 0, \$) = \{(q_2, 0\$)\}$: 如果在 $q_2$ 状态,输入'0',此时栈顶是栈底符号 $ (意味着这是第一个'0'),那么压入'0'。
- 上面两条可以合并为 $\delta(q_2, 0, \text{any}) = \{(q_2, 0\text{any})\}$,即只要输入是0,就压栈0。表格的 {(q2, 0)} 应该是 push 0 的简写,即用0替换栈顶,再压一个0。s_i = at, s_{i+1} = bt。δ(q, in, a) = (p,b)。如果 a=0, b=00,那就是 s_i=0t, s_{i+1}=00t。这表示压入一个0。
- $\delta(q_2, 1, 0) = \{(q_3, \varepsilon)\}$: 在 $q_2$ 状态,如果第一次看到输入 '1',并且栈顶有一个 '0',那么就弹栈 '0'(通过压入 $\varepsilon$ 实现),并转移到 $q_3$ 状态(读1阶段)。
- $\delta(q_3, 1, 0) = \{(q_3, \varepsilon)\}$: 在 $q_3$ 状态(读1阶段),如果继续看到输入 '1',并且栈顶有一个 '0',那么就继续弹栈 '0',并保持在 $q_3$。
- $\delta(q_3, \varepsilon, \$) = \{(q_4, \varepsilon)\}$**: 在 $q_3$ **状态**,如果输入已经读完(通过 $\varepsilon$ **转移**判断),并且**栈**顶是**栈底**符号 `$(意味着所有的'0'都被'1'抵消了),那么就弹栈** $`,并转移到最终的**接受状态** $q_4$。
- $q_4$: 这是一个“终点”状态,没有任何出边。
💡 [数值示例]
- 示例1: 接受 "0011" (我们来追踪一次成功的计算,并纠正表格中可能的笔误,采用更符合逻辑的流程,即先有一个 $\varepsilon$-转移压入$)
- 初始: (q1, 0011, ε)
- ε-转移: `(q2, 0011, $)` (假设有 $\delta(q_1, \varepsilon, \varepsilon)=\{(q_2, \$)\}$ )
- 读'0': `(q2, 011, 0$)` (根据 $\delta(q_2, 0, \$)=\{(q_2, 0\$)\}$ 的推广)
- 读'0': `(q2, 11, 00$)` (根据 $\delta(q_2, 0, 0)=\{(q_2, 00)\}$ 的推广)
- 读'1': `(q3, 1, 0$)` (根据 $\delta(q_2, 1, 0)=\{(q_3, \varepsilon)\}$ )
- 读'1': `(q3, ε, $)` (根据 $\delta(q_3, 1, 0)=\{(q_3, \varepsilon)\}$ )
- ε-转移: (q4, ε, ε) (根据 $\delta(q_3, \varepsilon, \$)=\{(q_4, \varepsilon)\}$ )
- 结束: 输入为空,自动机在状态 $q_4$。因为 $q_4 \in F$,所以字符串 "0011" 被接受。
- 示例2: 接受 $\varepsilon$ (n=0)
- 初始: (q1, ε, ε)
- 结束: 输入为空,自动机在状态 $q_1$。因为 $q_1 \in F$,所以空字符串 $\varepsilon$ 被接受。
- 初始: (q1, 011, ε)
- ε-转移: (q2, 011, $)
- 读'0': (q2, 11, 0$)
- 读'1': (q3, 1, $)
- 读'1': 此时状态是 $q_3$,输入是 '1',但栈顶是 `$`。表格中没有为 $\delta(q_3, 1, \$)$ 定义任何转移。因此,计算在此处“卡住”。不存在一条能走完整个字符串并到达接受状态的路径。所以字符串 "011" 被拒绝。
⚠️ [易错点]
- 表格表示法: 这种二维表格是转移函数的一种非标准但更紧凑的表示法。理解其行列的含义是关键。当遇到逻辑上不通顺或者有歧义的条目时(如此例中的 q1 行),结合上下文和语言本身的目标来推断其真实意图是很重要的。后面的状态图会提供更清晰的视角。
- 接受状态 $q_1$: 包含开始状态在接受状态集中,是处理空字符串 $\varepsilon$ 的一种常见技巧。
- 栈底符号 $`**: 这个**符号**至关重要,它让**PDA**知道两件事:1) 何时是第一个'0'(当**栈**里只有`$时读到'0');2) 何时所有的'0'都被匹配完了(当读完'1'后,栈**里只剩下$)。
📝 [总结]
本例给出了识别语言 $\{0^n1^n\}$ 的PDA $M_1$ 的完整形式化定义,包括其六元组和以表格形式呈现的转移函数。通过分析转移函数的规则,我们可以追踪PDA处理输入字符串(如"0011")时的状态和栈变化,验证其如何正确地接受属于该语言的字符串,并拒绝不属于该语言的字符串。
🎯 [存在目的]
本例的目的是将前面抽象的PDA形式定义(六元组、转移函数、计算三原则)与一个具体的、非平凡的语言识别问题联系起来。它展示了如何用这些形式化工具来精确地构建一个能解决该问题的自动机,是理论联系实际的第一次重要演练。
🧠 [直觉心智模型]
这个PDA $M_1$ 是一个有四个“心态”的机器人:
- $q_1$ (悠闲态): “我还没开始工作,如果啥也不干(输入$\varepsilon$),那就算我完成了。”
- $q_2$ (存钱态): “老板发工资了(输入'0'),我存一张钞票(压栈'0')。” 这个过程可以重复。当它看到第一笔支出(输入'1')时,它会转换心态。
- $q_3$ (花钱态): “看到一笔账单(输入'1'),我就花掉一张钞票(弹栈'0')。” 这个过程也可以重复。
- $q_4$ (退休态): “所有账单都付清了,所有存的钱也正好花完(栈空了),我终于可以退休了(接受)。”
💭 [直观想象]
想象一个天平:
- $q_2$ 阶段: 每输入一个'0',就在天平左边放一个砝码。
- $q_3$ 阶段: 每输入一个'1',就从天平左边拿走一个砝码。
- $q_4$ 阶段: 输入结束时,如果天平正好平衡(栈空),则成功。
- $q_1$: 特殊情况,如果一开始就没有任何输入,天平也是平衡的,也算成功。
- $ 符号: 就像天平的底座,用来标记“零点”。
📜 [原文18]
我们还可以使用状态图来描述 PDA,如图 2.15、2.17 和 2.19 所示。这些图类似于用于描述有限自动机的状态图,并进行了修改以显示 PDA 在状态之间转换时如何使用其栈。我们写“ $a, b \rightarrow c$ ”表示当机器正在从输入中读取 $a$ 时,它可以将栈顶的符号 $b$ 替换为 $c$。$a, b$ 和 $c$ 中的任何一个都可以是 $\varepsilon$。如果 $a$ 是 $\varepsilon$,机器可以在不读取任何输入符号的情况下进行此转移。如果 $b$ 是 $\varepsilon$,机器可以在不读取和弹栈任何符号的情况下进行此转移。如果 $c$ 是 $\varepsilon$,机器在沿着此转移时不会在栈上写入任何符号。

图 2.15
识别 $\left\{0^{n} 1^{n} \mid n \geq 0\right\}$ 的 PDA $M_{1}$ 的状态图
📖 [逐步解释]
这一段介绍了PDA的另一种描述方法——状态图,它比形式化的六元组或表格更直观。
- “我们还可以使用状态图来描述 PDA”:
- 状态图(State Diagram)是一种图形化表示方法,在有限自动机中已经广泛使用。对于PDA,我们只需要对状态图的转移标签进行扩展即可。
- “类似于用于描述有限自动机的状态图,并进行了修改以显示 PDA 在状态之间转换时如何使用其栈”:
- 相似之处: 仍然用圆圈表示状态,用箭头表示转移,用一个指向开始状态的无源箭头表示起点,用双层圆圈表示接受状态。
- 修改之处: 转移箭头上标记的不再仅仅是输入符号,而是一个更复杂的三元组,用来表示栈的操作。
- “我们写‘$a, b \rightarrow c$’表示...”:
- 这里定义了PDA状态图中转移标签的语法。
- $a \in \Sigma_{\varepsilon}$: 输入。要消耗的输入符号(或者是 $\varepsilon$)。
- $b \in \Gamma_{\varepsilon}$: 弹栈。转移前必须匹配的栈顶符号(或者是 $\varepsilon$)。
- $c \in \Gamma_{\varepsilon}$: 压栈。转移后要压入栈顶的新符号(或者是 $\varepsilon$)。
- 整个含义: 读入 $a$,看到栈顶是 $b$,就把它弹出,然后压入 $c$。这精确对应了条件2中的 $s_i=bt, s_{i+1}=ct$(这里原文用 $a,b,c$,我们用 $b,c$ 对应之前的 $a,b$ 来避免混淆)。如果 $s_i = B T$, $s_{i+1} = C T$,那么标签就是 input, B -> C。
- “$a, b$ 和 $c$ 中的任何一个都可以是 $\varepsilon$”:
- $a = \varepsilon$: $\varepsilon$-转移,不读输入,输入磁头不动。
- $b = \varepsilon$: 纯压栈操作。不要求栈顶有特定符号,也不弹出任何东西,直接压入 $c$。标签: $a, \varepsilon \to c$。
- $c = \varepsilon$: 纯弹栈操作。读入 $a$,匹配并弹出 $b$,不压入任何新东西。标签: $a, b \to \varepsilon$。
- 分析图 2.15:
- $q_1$: 开始状态,并且是接受状态(双圈)。这直接处理了 $n=0$ 的情况(输入 $\varepsilon$)。
- $q_1 \to q_2$ 的转移 $\varepsilon, \varepsilon \to \$$:
- 输入 $a=\varepsilon$: 不读输入。
- 弹栈 $b=\varepsilon$: 不看栈顶,也不弹出。
- 压栈 $c=\$$: 压入 栈底符号 '$'。
- 含义: 这是初始化步骤,从 $q_1$ 无条件地、在不读任何输入的情况下,压入 '$' 并进入**状态** $q_2$。
- $q_2$ 的自环 0, $\varepsilon \to 0$:
- 输入 $a=0$: 读一个 '0'。
- 弹栈 $b=\varepsilon$: 不弹出任何东西。
- 压栈 $c=0$: 压入一个 '0'。
- 含义: 每读到一个 '0',就往栈里压入一个 '0'。这是计数阶段。
- $q_2 \to q_3$ 的转移 1, $0 \to \varepsilon$:
- 输入 $a=1$: 读一个 '1'。
- 弹栈 $b=0$: 栈顶必须是 '0',然后将其弹出。
- 压栈 $c=\varepsilon$: 不压入任何东西。
- 含义: 第一次读到 '1' 时,开始消耗栈里的 '0',并进入匹配 '1' 的状态 $q_3$。
- $q_3$ 的自环 1, $0 \to \varepsilon$:
- 含义: 在 $q_3$ 状态,每继续读到一个 '1',就继续消耗一个栈里的 '0'。
- $q_3 \to q_4$ 的转移 $\varepsilon, \$ \to \varepsilon$:
- 输入 $a=\varepsilon$: 不读输入(意味着所有 '1' 都读完了)。
- 弹栈 $b=\$$: 栈顶必须是栈底符号 '$'(意味着所有 '0' 都被抵消了)。
- 压栈 $c=\varepsilon$: 不压入任何东西。
- 含义: 匹配成功后,清理栈,进入最终的接受状态 $q_4$。
- $q_4$: 是接受状态(双圈)。
💡 [数值示例]
- 初始: 在 $q_1$。输入 "01"。栈 []。
- 走 $\varepsilon, \varepsilon \to \$$ 转移: 到达 $q_2$。输入 "01"。栈 [$]。
- 走 $q_2$ 的自环 0, $\varepsilon \to 0$: 读'0'。到达 $q_2$。输入 "1"。栈 [0, $]。
- 走 $q_2 \to q_3$ 转移 1, $0 \to \varepsilon$: 读'1'。栈顶是'0',匹配,弹出。到达 $q_3$。输入 "" (空)。栈 [$]。
- 走 $q_3 \to q_4$ 转移 $\varepsilon, \$ \to \varepsilon$`: 不读输入。**栈顶**是'$',匹配,弹出。到达 $q_4$。输入 ""。栈 []`。
- 结束: 计算停在 $q_4$,是接受状态。接受。
⚠️ [易错点]
- 状态图与表格的对应: 仔细对比这个状态图和上一个例子中的表格,会发现状态图的逻辑更清晰,也修正了表格中可能存在的歧义或笔误。例如,状态图明确显示了从 $q_1$ 到 $q_2$ 的初始化步骤是一个独立的 $\varepsilon$-转移。
- $a, b \to c$ 的顺序: 必须牢记这个顺序代表 输入, 弹栈 -> 压栈。
📝 [总结]
本段介绍了使用状态图来可视化地描述PDA的方法。通过扩展转移箭头上的标签为 输入, 弹栈 -> 压栈 的格式,状态图能够直观地展示PDA的状态流转和栈操作。图2.15为识别 $\{0^n1^n\}$ 的PDA $M_1$ 提供了一个清晰、易于理解的状态图表示。
🎯 [存在目的]
本段的目的是提供一种比形式化定义和表格更友好的工具来设计和理解PDA。状态图将自动机的动态流程形象化,使读者能更容易地“跑”一个例子,并理解自动机设计的核心思想。它是理论和实践之间的重要桥梁。
🧠 [直觉心智模型]
PDA的状态图就像一个带有“特殊指令”的流程图。
- 圆圈 (状态): 你当前所处的“工作模式”(比如“正在收集数据模式”、“正在处理数据模式”)。
- 箭头 (转移): 从一种模式切换到另一种模式的“扳机”。
- 箭头上的标签 $a, b \to c$: 就是这个“扳机”的激活条件和效果。“当条件 $a$ 和 $b$ 同时满足时,触发此箭头,并执行效果 $c$”。
💭 [直观想象]
这就像一个复杂的地铁换乘图。
- 站点: 状态。
- 线路: 转移。
- 有限自动机: 乘坐地铁只需要一张票(输入)。“在A站,刷卡进站(读入符号),就可以上1号线到B站”。
- 下推自动机: 乘坐这个地铁还需要完成“任务”。线路上的指示牌写着:“在A站,刷卡进站(读入 $a$),并且你必须丢掉你背包里的苹果(弹出 $b$),然后系统会给你一个香蕉(压入 $c$),你才能上车去B站”。这个带任务的换乘系统就是一个PDA。
📜 [原文19]
PDA 的形式定义没有明确的机制允许 PDA 测试空栈。这个 PDA 能够通过最初在栈上放置一个特殊符号 $\
$$
来达到同样的效果。然后,如果它再次看到 $\
$$
,它就知道
栈实际上是空的。随后,当我们非正式地描述
PDA 时提及测试空
栈,我们以相同的方式实现该过程。
📖 [逐步解释]
这一段解释了一个在PDA设计中非常常用且重要的技巧:如何模拟“测试栈是否为空”。
- “PDA 的形式定义没有明确的机制允许 PDA 测试空栈”:
- 回顾转移函数 $\delta(q, a, x)$。它的第三个输入参数 $x$ 是栈顶符号。如果栈是空的,那么就没有“栈顶符号”了。形式定义中没有一个特殊的输入来代表“栈为空”。
- 我们只能匹配 $\Gamma$ 中的符号,或者用 $\varepsilon$ 来表示“不消耗栈顶符号”,但这不等于“测试到栈是空的”。
- “这个 PDA 能够通过最初在栈上放置一个特殊符号 \$ 来达到同样的效果”:
- 这里给出了解决方案:引入一个哨兵(Sentinel)或标记(Marker)。
- $**$: 这个符号被选为栈底标记。它必须是栈字母表** $\Gamma$ 的一个成员。
- 最初放置: 在PDA开始处理实际输入之前,第一步就是将 $` **压入**空**栈**。在图2.15中,这就是从 $q_1$ 到 $q_2$ 的 `$\varepsilon, \varepsilon \to \$$ 转移所做的事情。
- “然后,如果它再次看到 \$,它就知道栈实际上是空的”:
- 这里的“空”是指“除了我们的特殊标记$之外,其他所有工作符号都已经被弹出了”。
- 在计算过程中,$ 会一直静静地待在栈底。我们会把其他符号(如'0')压在它的上面。
- 当我们弹出了所有工作符号后,$ 就会重新暴露在栈顶。
- 因此,“栈顶是$”这个可检测的条件,就等价于“工作栈已空”这个我们想要知道的状态。
- 在图2.15中,$q_3 \to q_4$ 的转移 $\varepsilon, \$ \to \varepsilon$` 就是一个典型的例子。这个**转移**只有在所有的'0'都被'1'抵消后,`$ 重新成为栈顶时才能触发。
- “随后,当我们非正式地描述 PDA 时提及测试空栈,我们以相同的方式实现该过程”:
- 这是一个约定。作者告诉我们,以后为了方便,可能会直接说“...然后测试栈是否为空...”。当我们看到这种非正式描述时,我们应该在脑海里自动将其翻译成“...通过检查栈顶是否为栈底标记$来实现...”。
💡 [数值示例]
- 场景: 识别 $\{0^n1^n\}$,输入 "01"。
- 初始: 栈 []。
- 压入栈底标记: 栈 [$]。
- 读'0': 压入'0'。栈 [0, $]`。此时 `$ 被盖住了,不可见。
- 读'1': 弹出'0'。栈 [$]`。此时 `$ 重新暴露在栈顶。
- 测试“空栈”: PDA看到栈顶是 $,它就知道所有用于计数的'0'都用完了。任务完成。
- 如果不用$会怎样?
- 场景: 识别 $\{0^n1^n\}$,输入 "01",没有$。
- 初始: 栈 []。
- 读'0': 压入'0'。栈 [0]。
- 读'1': 弹出'0'。栈 []。
- 问题: 现在栈空了。我们如何区分这是“成功匹配完成”的空(如"01"之后),还是“0比1少,提前用完”的空(如处理"011"到第二个'1'时)?
- 如果转移规则是“读'1',弹'0'”,那么在处理"011"的第二个'1'时,栈已经空了,没有'0'可弹,计算卡住,拒绝。这似乎可行。
- 但$提供了更强的保证和更清晰的逻辑。例如,它可以防止在不该弹空栈的时候意外弹空。它提供了一个明确的“终点”信号,使得PDA的设计可以更加鲁棒。
⚠️ [易错点]
- $`的选择**: **栈底**标记 `$ 必须是一个不会与任何其他工作符号混淆的符号。通常它不属于输入字母表** $\Sigma$。
- 忘记初始化: 设计PDA时,一个常见的错误是忘记第一步压入$。这会导致后续的“空栈测试”逻辑失效。
📝 [总结]
本段解释了PDA中一个关键的设计模式:使用一个特殊的栈底标记(如$)来模拟“测试栈是否为空”的功能。通过在计算开始时压入该标记,并在计算结束时检查该标记是否重新出现在栈顶,PDA可以有效地判断其工作栈是否已被清空。
🎯 [存在目的]
本段的目的是为了弥合PDA形式定义与实际设计需求之间的差距。形式定义本身没有提供“测试空栈”的原语操作,本段则提供了一个标准的、通用的“软件”实现方法。这使得我们在更高层次上思考和设计PDA算法成为可能。
[直觉心-智模型]
这就像你在一个深不见底的井里寻宝。
- 没有$: 你不知道井里到底还有没有宝物(符号),也不知道什么时候到底了。你只能一直往下捞,直到什么也捞不上来,但你分不清是暂时没捞到,还是真的到底了。
- 有$`**: 在开始寻宝前,你先往井里扔了一块会发光的石头(`$)。然后开始放宝物。当你把所有宝物都捞上来后,你会看到井底那块发光的石头。那一刻,你就确信无疑:所有的宝物都捞完了。这块发光的石头就是你的栈底**标记。
💭 [直观想象]
这就像写一本书。
- 在书的第一页(压入$)写上“本书开始”。
- 然后你写了很多章节内容(压入其他符号)。
- 当你审阅并删改内容时(弹出),你一页一页地往前翻。
- 当你翻到写着“本书开始”的那一页时,你就知道你已经回到了书的起点,所有内容都处理完了。
📜 [原文20]
类似地,PDA 无法明确测试是否已到达输入字符串的末尾。这个 PDA 能够达到这种效果,因为接受状态仅在机器位于输入末尾时才生效。因此,从现在开始,我们假设 PDA 可以测试输入的末尾,并且我们知道我们可以以相同的方式实现它。
📖 [逐步解释]
这一段解释了PDA如何隐式地“测试输入是否结束”,与上一段解释如何“测试栈是否为空”相对应。
- “类似地,PDA 无法明确测试是否已到达输入字符串的末尾”:
- PDA的转移函数 $\delta(q, a, x)$ 中的输入符号 $a$ 只能是 $\Sigma$ 中的一个符号,或者是 $\varepsilon$。没有一个特殊的输入符号来代表“END OF INPUT”(输入结束)。
- 自动机是“目光短浅”的,它在任何时候只能看到磁头下的那一个符号,它不知道后面还有多长。
- “这个 PDA 能够达到这种效果,因为接受状态仅在机器位于输入末尾时才生效”:
- 这里揭示了实现方法。这不是通过一个特殊的转移操作,而是通过“接受”的定义本身来实现的。
- 回顾计算和接受的定义:一个字符串 $w$ 被接受,当且仅当存在一个计算序列 $w=w_1w_2...w_m$,使得在消耗完所有 $w_i$ 后,自动机到达一个接受状态 $r_m \in F$。
- “消耗完所有 $w_i$” 就意味着“位于输入末尾”。
- 所以,接受状态的检查这个动作,本身就发生在计算的终点,也就是输入结束的时刻。
- “因此,从现在开始,我们假设 PDA 可以测试输入的末尾,并且我们知道我们可以以相同的方式实现它”:
- 这又是一个约定,为了方便后续的非正式描述。
- 当我们说“...直到输入末尾...”或者“...如果输入结束...”,我们不是指PDA有一个神奇的预知能力。
- 我们的意思是,相关的转移(特别是那些导向最终接受状态的转移)应该被设计成在消耗完所有有意义的输入后,通过一系列 $\varepsilon$-转移来触发,而最终的接受检查是在整个计算序列的末端进行的。
💡 [数值示例]
- 场景: 识别 $\{0^n1^n\}$,输入 "01"。
- (q1, 01, ε) -> (q2, 01, $)
- (q2, 1, 0$)` -> `(q3, 1, 0$) -> (q3, ε, $)
- 此时,有意义的输入 '0' 和 '1' 都被消耗完了。输入带逻辑上是空的。
- PDA现在可以执行 $\varepsilon$-转移了。它执行 $q_3 \to q_4$ 的转移 $\varepsilon, \$ \to \varepsilon$。
- 到达 $q_4$。现在,计算的一个分支到达了终点。系统会检查:1) 输入是否真的都处理完了?是的。2) 当前状态 $q_4$ 是否在接受状态集 $F$ 中?是的。
- 因此,字符串被接受。
- “测试输入末尾”这个动作,体现在第4步的 $\varepsilon$-转移上。这个转移只有在真正的输入符号都处理完之后才有意义。如果输入是 "010",在第3步之后,PDA仍然可以尝试走 `$\varepsilon, \$ \to \varepsilon$` 这条路,但即使走到了 $q_4$,整个字符串并没有被完全消耗(还剩一个'0'),所以这个计算路径是非法的。
⚠️ [易错点]
- 易错点: 认为PDA有一个 is_end_of_input() 的函数。它没有。这个“测试”是计算模型定义的结果,而不是一个主动操作。
- $\varepsilon$的角色: $\varepsilon$-转移在“测试输入末尾”中扮演了重要角色。通常,一个PDA在读完所有实际符号后,会通过一个或多个 $\varepsilon$-转移来做最后的清理(比如清空栈底标记)和状态调整,最终进入一个“干净”的接受状态。这些 $\varepsilon$-转移就是在“输入末尾”执行的。
📝 [总结]
本段阐明,PDA通过其“接受”定义来隐式地“测试输入是否结束”。接受的条件是在整个输入字符串被完全处理之后,自动机恰好处于一个接受状态。因此,我们可以非正式地假设PDA能测试输入末尾,因为其最终的接受判决机制天然地保证了这一点。
🎯 [存在目的]
本段的目的是消除另一个可能的困惑,即PDA如何知道计算何时结束。与“测试空栈”的技巧相平行,这里解释了“测试输入结束”是通过计算模型的定义本身来保证的。这使得我们可以更自由、更高效地进行非形式化的算法描述,而不必每次都拘泥于底层的形式化细节。
🧠 [直觉心智模型]
这就像跑步比赛的终点线。
- 运动员(自动机)在跑道上奔跑(处理输入)。他不知道终点线在哪里。
- 终点线处有一个裁判(接受状态检查机制)。
- 只有当运动员冲过终点线(输入结束)的那一刻,裁判才会看他是不是第一个(是否在接受状态)。
- 运动员自己不需要“测试”终点线在哪里,他只管跑。是“冲过终点线”这个事件,触发了“被裁判检查”这个动作。
💭 [直观想象]
你正在阅读一本书。
- 你一页一页地读(处理输入)。你不知道最后一页是哪一页。
- 当你翻到某一页,发现后面再也没有任何内容了(输入结束),你合上书。
- 此时,你根据你对整本书的感受,决定你“喜欢”(接受)还是“不喜欢”(拒绝)这本书。
- 你不是在每一页都问“这是最后一页吗?”。而是“读完”这个事实,自然而然地让你做出了最终的评判。
3.2. 例子 2.16
📜 [原文21]
此示例说明了一个识别语言的下推自动机
$$
\left\{\mathrm{a}^{i} \mathrm{~b}^{j} \mathrm{c}^{k} \mid i, j, k \geq 0 \text { 且 } i=j \text { 或 } i=k\right\} 。
$$
非正式地,这种语言的 PDA 首先读取并压栈 a。当 a 结束时,机器将所有 a 放在栈上,这样它就可以将它们与 b 或 c 匹配。这个操作有点棘手,因为机器事先不知道是与 b 还是与 c 匹配 a。非确定性在这里派上了用场。
📖 [逐步解释]
这一段引入了一个更复杂的语言,并点出了识别这个语言的关键难点和所需要的核心技术——非确定性。
- 语言定义:
- $\left\{\mathrm{a}^{i} \mathrm{~b}^{j} \mathrm{c}^{k} \mid i, j, k \geq 0 \text { 且 } i=j \text { 或 } i=k\right\}$:
- 字符串结构: 由三部分组成,一串'a',跟着一串'b',再跟着一串'c'。顺序是固定的 a...ab...bc...c。
- $i, j, k \ge 0$: 'a', 'b', 'c' 的数量都可以是0。
- 核心条件: $i=j$ 或 $i=k$: 'a'的数量必须等于'b'的数量,或者等于'c'的数量。两者满足其一即可。
- 语言中的字符串示例:
- aabc (i=2, j=1, k=1) -> 不属于,因为 $2 \ne 1$。
- aabbc (i=2, j=2, k=1) -> 属于,因为 $i=j$。
- aacc (i=2, j=0, k=2) -> 属于,因为 $i=k$。
- abbc (i=1, j=2, k=1) -> 属于,因为 $i=k$。
- abc (i=1, j=1, k=1) -> 属于,因为 $i=j$ (也满足 $i=k$)。
- b (i=0, j=1, k=0) -> 属于,因为 $i=j=0$ (0个a和0个b是相等的)。严格来说,$i=0, j=1$不满足$i=j$。但如果$i=0$,那么 $i=k=0$ (假设k=0)或 $i=j=0$ (假设j=0) 是满足的。例如 $i=0, j=0, k=5$ 的 "ccccc" 是合法的。$i=0, j=1, k=0$ 的 "b" 不合法。$i=1,j=0,k=0$ 的 "a" 也不合法。
- 非正式描述PDA策略:
- 第一步: “首先读取并压栈 a”: 这和识别 $\{0^n1^n\}$ 类似。PDA需要将'a'的数量记录下来,而栈是完美的工具。每读一个'a',就往栈里压入一个符号。
- 第二步: “当 a 结束时...将它们与 b 或 c 匹配”: 当PDA开始看到'b'或'c'时,它需要用栈里存的'a'的数量来进行比较。每读一个'b'或'c',就弹出一个之前存的'a'。
- 难点: “机器事先不知道是与 b 还是与 c 匹配 a”:
- 这是这个问题的核心。当PDA读完所有的'a',栈里装满了代表'a'的符号,然后它看到了第一个非'a'符号。
- 如果这个符号是'b',PDA应该开始用'b'来消耗栈里的计数。
- 如果这个符号是'c'(可能因为 $j=0$),PDA应该忽略所有的'b',等待'c'出现,然后用'c'来消耗栈。
- 问题在于,当它开始读'b'的时候,它无法预知后面的'c'的数量是否也和'a'相等。它必须做一个选择:是现在就开始用'b'匹配,还是跳过'b',等待用'c'来匹配?
- 一个确定性的PDA在此时会陷入困境,因为它必须做出一个唯一的选择,而它没有足够的信息来保证这个选择是正确的。
- 解决方案: “非确定性在这里派上了用场”:
- 非确定性允许PDA“两边下注”。当读完'a'之后,PDA可以非确定性地分裂成两条计算路径:
- 路径1 (赌 $i=j$): 这条路径会假设'a'要和'b'匹配。它会开始用'b'来弹栈。如果'a'和'b'成功匹配完(栈空),它就高兴地忽略掉后面所有的'c',然后接受。如果匹配失败,这条路就死亡。
- 路径2 (赌 $i=k$): 这条路径会假设'a'要和'c'匹配。它会选择忽略掉所有'b'(不进行任何栈操作),然后等待'c'的出现。一旦看到'c',它就开始用'c'来弹栈。如果'a'和'c'成功匹配完,就接受。如果匹配失败,这条路也死亡。
- 只要输入的字符串满足 $i=j$ 或 $i=k$ 中的任何一个条件,那么对应的路径1或路径2中就至少会有一条能够成功走到接受状态。根据非确定性的接受定义,整个PDA就接受这个字符串。
💡 [数值示例]
- 示例1: 输入 "abbc" (i=1, j=2, k=1)
- 读'a',压栈'X'。栈: [X]。
- 读到'b'。PDA分裂:
- 路径1 (赌 i=j):
- 读'b',弹栈'X'。栈: []。
- 再读'b',想弹栈,但栈空了。此路死亡。
- 路径2 (赌 i=k):
- 读'b',忽略,不操作栈。栈: [X]。
- 再读'b',忽略,不操作栈。栈: [X]。
- 读'c',弹栈'X'。栈: []。
- 输入结束,栈空(假设有$机制)。成功。
- 因为路径2成功了,所以 "abbc" 被接受。
- 示例2: 输入 "aabc" (i=2, j=1, k=1)
- 读两个'a',压栈两个'X'。栈: [X, X]。
- 读到'b'。PDA分裂:
- 路径1 (赌 i=j):
- 读'b',弹栈'X'。栈: [X]。
- 读'c',路径1的逻辑是匹配b,所以看到c会卡住或忽略。输入结束,栈不空。此路死亡。
- 路径2 (赌 i=k):
- 读'b',忽略。栈: [X, X]。
- 读'c',弹栈'X'。栈: [X]。
- 输入结束,栈不空。此路死亡。
- 所有路径都死亡了,所以 "aabc" 被拒绝。
⚠️ [易错点]
- 易错点: 试图用一个确定性的思路去解决。比如“我先用b来匹配,如果失败了,我再退回去试试用c匹配”。PDA是不能“退回”的。非确定性不是“试错和回溯”,而是“同时探索所有可能”。
- 边界情况:
- $i=j=k$: 此时两条路径都会成功。
- $j=0$: PDA读完'a'后直接看到'c'。此时路径1(赌i=j)会因为没有'b'而可能直接卡住,但路径2(赌i=k)会正常工作。
- $i=0$: PDA不压栈。字符串可以是 $b^j c^k$。此时 $i=0$,那么条件 $i=j$ 或 $i=k$ 要求 $j=0$ 或 $k=0$。所以合法的字符串是 $c^k$ 或 $b^j$。PDA需要能正确处理这种情况。
📝 [总结]
本段通过引入一个包含“或”逻辑条件的语言 $\{a^ib^jc^k \mid i=j \text{ or } i=k\}$,生动地展示了非确定性在PDA中的关键作用。由于PDA在处理过程中无法预知未来,它必须在“用b匹配a”和“用c匹配a”之间做出选择。非确定性允许PDA同时探索这两种可能性,只要其中之一成功,就能正确接受字符串。
🎯 [存在目的]
本例的目的是提供一个具体的、有说服力的案例,来证明为什么非确定性对PDA来说不仅仅是方便,而是本质性的能力提升。它为之前“NPDA比DPDA更强”的论断提供了第一个强有力的证据。
🧠 [直觉心智模型]
你是一个投资人,有一笔钱(栈里的'a')。有两个项目B和C向你融资。合同规定,你必须把所有的钱投给B或C中的一个,才能获得回报。
- 确定性投资人: 他必须先选一个,比如B。他把钱全给了B,然后发现B项目失败了。他没钱了,也错过了C项目,最终破产(拒绝)。
- 非确定性投资人: 他可以“分裂”出两个平行世界的自己。一个自己把所有钱投给B,另一个自己把所有钱投给C。只要在其中任何一个平行世界里,他投资的项目成功了(匹配成功),那么在更高的维度上,这次投资就算成功了(接受)。
💭 [直观想象]
你走到了一个岔路口。左边的路牌写着“通往罗马”,右边的路牌也写着“通往罗马”。你知道其中一条是骗人的,但你不知道是哪条。
- DPDA: 你只能选一条路走到底。选错了就到不了罗马。
- NPDA: 你可以派一个克隆人走左边,你自己走右边。只要你们俩有一个到了罗马,就算任务完成。
📜 [原文22]
PDA 利用其非确定性,可以猜测是与 b 还是与 c 匹配 a,如图 2.17 所示。可以把机器想象成它的非确定性有两个分支,每个分支对应一个可能的猜测。如果其中任何一个匹配,该分支接受,并且整个机器接受。问题 2.27 要求您证明非确定性对于 PDA 识别这种语言至关重要。

图 2.17
识别 $\left\{\mathrm{a}^{i} \mathrm{~b}^{j} \mathrm{c}^{k} \mid i, j, k \geq 0 \text { 且 } i=j \text { 或 } i=k\right\}$ 的 PDA $M_{2}$ 的状态图
📖 [逐步解释]
这一段结合一个状态图,具体展示了如何利用非确定性来构建识别上一个例子中语言的PDA。
- “PDA 利用其非确定性,可以猜测是与 b 还是与 c 匹配 a”: 再次强调核心策略。
- “可以把机器想象成它的非确定性有两个分支...”: 提供了理解非确定性计算过程的心智模型:分支或并行宇宙。
- 分析图 2.17 ($M_2$):
- 初始部分 ($q_1 \to q_2$):
- $q_1$: 开始状态,也是接受状态(处理i=j=k=0的空串,以及i=0的情况)。
- $q_1 \to q_2$ 的 $\varepsilon, \varepsilon \to \$$ : 初始化,压入 栈底符号 $。
- 读取 'a' 的部分 ($q_2$ 的自环):
- $q_2$ 上的 a, $\varepsilon \to a$ : 每读一个'a',就压入一个'a'到栈中。这是计数阶段。
- 非确定性分裂点 ($q_2$ 的出边):
- 当在状态 $q_2$ 读完所有'a'后,输入将是'b'或'c'。PDA通过两条从 $q_2$ 出发的 $\varepsilon$-转移来实现“猜测”。
- $q_2 \to q_3$ 的转移 $\varepsilon, \varepsilon \to \varepsilon$: 这是通往“匹配b”分支的路径。
- $q_2 \to q_5$ 的转移 $\varepsilon, \varepsilon \to \varepsilon$: 这是通往“匹配c”分支的路径。
- 当PDA处于 $q_2$ 并且下一个输入不是'a'时,它可以在不消耗任何输入的情况下,非确定性地选择进入 $q_3$ 或 $q_5$。
- 分支1: 匹配 a 和 b ($q_3, q_4$):
- $q_3$ 上的 b, $a \to \varepsilon$ : 每读一个'b',就消耗栈里的一个'a'。
- $q_3 \to q_4$ 的 $\varepsilon, \$ \to \varepsilon$` : 当'b'读完,如果**栈**里只剩**栈底** `$ (说明 a, b 数量相等),就进入 $q_4$。
- $q_4$ 上的 c, $\varepsilon \to \varepsilon$ : 在 $q_4$ 状态,意味着 a 和 b 已经匹配成功。此时可以忽略后面所有的 'c'。
- $q_4$ 是接受状态。所以如果能到达这里,说明 $i=j$ 满足。
- 分支2: 匹配 a 和 c ($q_5, q_6, q_7$):
- $q_5$ 上的 b, $\varepsilon \to \varepsilon$ : 这条路径的目标是匹配 a 和 c,所以它会忽略掉所有的 'b'。
- $q_5 \to q_6$ 的 $\varepsilon, \varepsilon \to \varepsilon$ : 忽略完所有'b'之后,通过一个 $\varepsilon$-转移进入准备匹配'c'的状态 $q_6$。
- $q_6$ 上的 c, $a \to \varepsilon$ : 每读一个'c',就消耗栈里的一个'a'。
- $q_6 \to q_7$ 的 $\varepsilon, \$ \to \varepsilon$` : 当'c'读完,如果**栈**里只剩**栈底** `$ (说明 a, c 数量相等),就进入 $q_7$。
- $q_7$ 是接受状态。所以如果能到达这里,说明 $i=k$ 满足。
- “问题 2.27 要求您证明...”: 这是一个课后习题的提示,暗示这个语言是非确定性上下文无关语言的经典例子。
💡 [数值示例]
- 示例: 追踪 "ac" (i=1, j=0, k=1)
- 初始: (q1, ac, ε)
- ε,ε→$` : `(q2, ac, $)
- a,ε→a : (q2, c, a$)
- 非确定性分裂:
- 路径1 (去 $q_3$): `(q3, c, a$)`。在 $q_3$ 看到输入'c',没有定义转移。此路死亡。
- 路径2 (去 $q_5$): `(q5, c, a$)`。在 $q_5$ 看到输入'c',没有 `c` 的**转移**。但可以先走 $\varepsilon$-转移。
- ε,ε→ε ($q_5 \to q_6$): (q6, c, a$)。
- c,a→ε ($q_6$自环): (q6, ε, $)。
- ε,$→ε` ($q_6 \to q_7$): (q7, ε, ε)`。
- 到达接受状态 $q_7$。此路成功。
- 结论: 因为存在一条成功路径,"ac" 被接受。
⚠️ [易错点]
- $\varepsilon$-转移链: 注意到分支2中,从 $q_5$ 到 $q_6$ 也是一个 $\varepsilon$-转移。这意味着在读完'b'和开始读'c'之间,自动机可以进行一次无输入的状态转换。这种设计使得逻辑更清晰。
- 忽略环节: 分支1中的 $q_4$ 自环 c,ε→ε 和分支2中的 $q_5$ 自环 b,ε→ε 是“忽略”操作,它们只消耗输入,不改变栈,确保匹配完成后,剩余的符号不会导致计算失败。
📝 [总结]
本段通过一个结构清晰的状态图,详细演示了如何使用非确定性来解决识别 $\{a^ib^jc^k \mid i=j \text{ or } i=k\}$ 的问题。其核心思想是在读完'a'后,通过 $\varepsilon$-转移分裂出两个独立的计算分支,一个负责验证 $i=j$,另一个负责验证 $i=k$。只要有任一分支成功,PDA就能接受该字符串。
🎯 [存在目的]
本段的目的是将上一段提出的非正式策略,转化为一个具体、可视化的PDA设计。通过状态图,读者可以清楚地看到非确定性是如何通过状态和转移来实现的,从而深化对非确定性强大能力的理解。
[直觉心-智模型]
这个PDA是一个有两个“专家团队”的检测机构。
- $q_2$: 前台接待,负责登记'a'的数量(把'a'都记在栈上)。
- 接待员完成登记后,把任务分发给两个团队(非确定性分裂)。
- 团队1 ($q_3, q_4$): “a-b匹配专家组”。他们只关心'b',用'b'去核销'a'的记录。如果核销成功,他们就宣布通过,并且不再理会'c'(由 $q_4$ 的自环完成)。
- 团队2 ($q_5, q_6, q_7$): “a-c匹配专家组”。他们完全不理会'b'(由 $q_5$ 的自环完成),只用'c'去核销'a'的记录。如果成功,他们宣布通过。
只要有一个团队宣布通过,整个机构就判定字符串合格。
💭 [直观想象]
你写了一篇论文,有两种发表途径:期刊A或期刊B。
- 写论文: 压栈'a'。
- 写完后,你把论文同时投给A和B(非确定性分裂)。
- 期刊A的审稿流程 ($q_3, q_4$): 他们主要看你的实验数据'b'。如果数据'b'能支撑你的论点'a',他们就接受你的论文,至于你的结论'c'写得好不好,他们不太在乎。
- 期刊B的审稿流程 ($q_5, q_6, q_7$): 他们不看重实验数据'b',直接跳过,主要看你的理论推导'c'。如果推导'c'能支撑你的论点'a',他们就接受。
只要有一家期刊接受了你的论文,你就成功发表了。
3.3. 例子 2.18
📜 [原文23]
在此示例中,我们给出一个识别语言 $\left\{w w^{\mathcal{R}} \mid w \in\{0,1\}^{*}\right\}$ 的 PDA $M_{3}$。回想一下,$w^{\mathcal{R}}$ 表示 $w$ 的反向。PDA 的非正式描述和状态图如下。
首先将读取的符号压栈。在每一点,非确定性地猜测已到达字符串的中间,然后切换到每读取一个符号就弹栈,检查它们是否相同。如果它们总是相同的符号,并且栈在输入完成时同时清空,则接受;否则拒绝。

图 2.19
识别 $\left\{w w^{\mathcal{R}} \mid w \in\{0,1\}^{*}\right\}$ 的 PDA $M_{3}$ 的状态图
📖 [逐步解释]
这个例子介绍了另一个经典的、需要非确定性的语言——偶数长度的回文串,并给出了其PDA的设计。
- 语言定义:
- $\left\{w w^{\mathcal{R}} \mid w \in\{0,1\}^{*}\right\}$:
- $w \in \{0,1\}^*$: $w$ 是由 '0' 和 '1' 组成的任意字符串(包括空串)。
- $w^R$: $w$ 的反转。如果 $w = w_1w_2...w_k$,那么 $w^R = w_k...w_2w_1$。
- $ww^R$: 整个字符串由前半部分 $w$ 和后半部分 $w$ 的反转构成。这一定义确保了整个字符串是一个偶数长度的回文串。
- 示例:
- $w=011$, $w^R=110$ -> 字符串 "011110" 属于该语言。
- $w=10$, $w^R=01$ -> 字符串 "1001" 属于该语言。
- $w=\varepsilon$ -> 字符串 "$\varepsilon$" 属于该语言。
- "101" 不属于,因为它是奇数长度。
- "1011" 不属于,因为它不是回文。
- 非正式描述PDA策略:
- “首先将读取的符号压栈”: 这是前半部分 $w$ 的处理。PDA把 $w$ 的内容忠实地记录在栈上。因为栈是 LIFO 的,如果 $w=w_1w_2...w_k$ 被压栈,栈顶将是 $w_k$,然后是 $w_{k-1}$,以此类推。栈里的内容从上到下正好是 $w$ 的反转 $w^R$。
- “在每一点,非确定性地猜测已到达字符串的中间”: 这是问题的核心难点和解决方案。字符串中没有像 # 这样的中点标记。因此,DPDA无法知道何时 $w$ 结束,$w^R$ 开始。NPDA则可以在读取每一个符号后都进行“猜测”。
- “然后切换到每读取一个符号就弹栈,检查它们是否相同”: 这是后半部分 $w^R$ 的处理。PDA进入匹配模式。对于 $w^R$ 的第一个符号(即$w_k$),它应该和栈顶(也是$w_k$)匹配。对于 $w^R$ 的第二个符号(即$w_{k-1}$),它应该和新的栈顶(也是$w_{k-1}$)匹配,以此类推。
- “如果它们总是相同的符号,并且栈在输入完成时同时清空,则接受”: 这是接受条件。所有符号都匹配,并且 $w$ 和 $w^R$ 的长度也一样(由栈是否清空来保证)。
- 分析图 2.19 ($M_3$):
- $q_1$: 开始状态和接受状态。接受空字符串 $\varepsilon$。
- $q_1 \to q_2$ 的 $\varepsilon, \varepsilon \to \$$: 初始化,压入 栈底符号。
- $q_2$ 的自环: 0, $\varepsilon \to 0$ 和 1, $\varepsilon \to 1$。
- 含义: 在 $q_2$ 状态,不断读取 '0' 或 '1',并将其压入栈中。这是在处理前半部分 $w$。
- $q_2 \to q_3$ 的 $\varepsilon, \varepsilon \to \varepsilon$:
- 含义: 这是非确定性的“猜测中点”转移。在 $q_2$ 的任何时候(读完一个符号后),PDA都可以选择不读输入,不操作栈,直接跳转到“匹配模式”状态 $q_3$。
- $q_3$ 的自环: 0, $0 \to \varepsilon$ 和 1, $1 \to \varepsilon$。
- 含义: 在 $q_3$ 状态,开始匹配后半部分 $w^R$。如果输入是'0',栈顶也必须是'0',匹配后弹栈。对'1'同理。如果输入和栈顶不匹配,则该计算路径死亡。
- $q_3 \to q_4$ 的 $\varepsilon, \$ \to \varepsilon$:
- 含义: 如果在 $q_3$ 状态,输入已经读完,并且栈中只剩栈底符号 $`,说明 $w$ 和 $w^R$ 完美匹配。此时**弹出** `$,进入最终的接受状态 $q_4$。
- $q_4$: 接受状态。
💡 [数值示例]
- 初始: (q1, 1001, ε) -> 接受 $\varepsilon$
- ε,ε→$` : `(q2, 1001, $)
- 非确定性选择:
- 猜测1 (中点在开头): 立即跳到 $q_3$。`(q3, 1001, $)`。读'1',**栈顶**是'$',不匹配。死亡。
- 猜测2 (中点在'1'后): 在 $q_2$ 读'1'。
- 1,ε→1 : (q2, 001, 1$)。
- 此时再猜测中点: 跳到 $q_3$。(q3, 001, 1$)。读'0',栈顶是'1',不匹配。死亡。
- 猜测3 (中点在'10'后): 在 $q_2$ 读'1',再读'0'。
- 1,ε→1 : (q2, 001, 1$)。
- 0,ε→0 : (q2, 01, 01$)。
- 此时进行正确的猜测: ε,ε→ε ($q_2 \to q_3$) -> (q3, 01, 01$)。
- 读'0',栈顶是'0',匹配。0,0→ε -> (q3, 1, 1$)。
- 读'1',栈顶是'1',匹配。1,1→ε -> (q3, ε, $)。
- 输入结束,走 ε,$→ε` ($q_3 \to q_4$) -> (q4, ε, ε)`。
- 到达接受状态 $q_4$。成功。
- 结论: 因为存在一条成功路径(猜测中点在"10"之后),所以 "1001" 被接受。
⚠️ [易错点]
- 非确定性的威力: 这个例子比上一个更能体现非确定性的“猜测”本质。PDA不是在 $w$ 和 $w^R$ 的分界处才做选择,而是在每一步都可以选择“现在是不是中点?”。
- 奇数长度回文串: 对于 "101",PDA可以猜测中点在'0'之后,然后用'1'匹配栈里的'1',但结束后输入为空,栈也为空,它会接受。为了严格识别 $ww^R$,这个PDA的设计隐含了只接受偶数长度。实际上,奇数长度回文 $\{w c w^R\}$ 可以被确定性 PDA 识别,因为有明确的中点标记 $c$。
📝 [总结]
本例通过语言 $\{ww^R\}$ 展示了PDA如何使用非确定性来解决“没有明确分隔符的对称匹配”问题。其核心策略是:在压栈阶段的每一步,都非确定性地“猜测”中点是否已到,并分裂出一条进入“弹栈匹配”阶段的计算路径。只要有一个猜测是正确的,PDA就能成功识别回文串。
🎯 [存在目的]
本例的目的是进一步强化非确定性对于PDA能力的重要性。与上一个例子中“二选一”的非确定性不同,这里的非确定性是“多选一”(在每个可能的中间点进行猜测),这是一种更强大的应用模式,也更清晰地说明了为什么DPDA无法胜任此类任务。
🧠 [直觉心智模型]
这就像在黑暗中找一堵镜子的中心点。
- 你从镜子的一端开始,一边走一边用手触摸镜面(压栈)。
- DPDA: 它必须找到一个确切的信号告诉它“这里是中心”,但镜子表面是光滑的,没有这样的信号。
- NPDA: 它的每一步都可以想:“我假设这里就是中心点”。然后它从这一点开始往回走,一边走一边回忆自己来时的路(弹栈匹配)。它在脑海中(计算空间)同时进行了所有可能的“假设”。只要有一个“假设”能让它完美地原路返回到起点,它就知道这面镜子是对称的。
💭 [直观想象]
你正在听一段录音,据说是“正放一遍,然后倒放一遍”的结构($ww^R$)。
- 你一边听前半段,一边在脑子里记下旋律(压栈)。
- 因为你不知道前半段什么时候结束,所以你每听一个音符,都在心里默默地启动一个“秒表”,开始将后续的音符与你记忆中的旋律倒序匹配(非确定性地猜测中点)。
- 你脑子里同时运行着很多个这样的“匹配进程”。
- 如果最后,有一个“秒表”恰好在你听完录音时,完美地匹配了所有音符,你就确认了这段录音确实是 $ww^R$ 的结构。
📜 [原文24]
问题 2.28 表明这种语言需要一个非确定性 PDA。$\square$
📖 [逐步解释]
这是一个结论性的陈述,并指引读者去思考相关的证明。
- “问题 2.28 表明这种语言需要一个非确定性 PDA”:
- 这明确地告诉我们,语言 $L = \{ww^R \mid w \in \{0,1\}^*\}$ 是一个非确定性上下文无关语言的实例。
- “需要”意味着不存在任何一个确定性下推自动机(DPDA)能够识别这个语言。
- 证明的思路(非正式): 假设存在一个DPDA可以识别 $L$。那么对于形如 $0^n110^n$ 的字符串,DPDA在读取到某个 $0^k$ 时,必须确定性地决定是继续压栈还是开始弹栈。但是,无论它如何决策,我们总能构造出另一个字符串(比如 $0^{n+m}110^{n+m}$ 或改变后半部分),使得这个DPDA的确定性决策出错。它无法仅根据已看到的前缀来唯一确定中点的位置。
- “$\square$”:
- 这个符号通常在教科书和数学文献中表示“证明结束”或“示例结束”。这里它标记了例子2.18的结束。
📝 [总结]
本句作为一个结语,断言了语言 $\{ww^R\}$ 的非确定性本质,并将其证明作为一个练习留给读者。
🎯 [存在目的]
本句的目的是为了给例子2.18画上一个句号,并再次强调本例的核心教学点:非确定性的必要性。通过将其表述为一个待证明的问题,可以激发读者更深入地思考为什么确定性模型在此处会失败。
44. 与上下文无关文法的等价性
4.1. 与上下文无关文法的等价性
📜 [原文25]
在本节中,我们展示了上下文无关文法和下推自动机在能力上是等价的。两者都能够描述上下文无关语言的类。我们展示了如何将任何上下文无关文法转换为识别相同语言的下推自动机,反之亦然。回想一下,我们将上下文无关语言定义为可以用上下文无关文法描述的任何语言,我们的目标是以下定理。
📖 [逐步解释]
这是“等价性”这一核心章节的开篇,它设定了本节的目标和路线图。
- “在本节中,我们展示了上下文无关文法和下推自动机在能力上是等价的”:
- 明确本节的核心主题:证明 CFG ~ PDA (在能力上等价)。
- 这是计算理论中一个里程碑式的定理,它将两种看起来截然不同的模型——一个基于规则的生成模型(CFG)和一个基于机器的识别模型(PDA)——紧密地联系在了一起。
- “两者都能够描述上下文无关语言的类”:
- 这是对“能力等价”的另一种表述。上下文无关语言(CFL)这个大家族,既可以用CFG作为其“户口本”(生成所有成员),也可以用PDA作为其“门卫”(识别所有成员)。
- “我们展示了如何将任何上下文无关文法转换为识别相同语言的下推自动机,反之亦然”:
- 这揭示了证明等价性的方法:构造性证明。
- 我们将要学习两个算法(或称“程序”):
- 算法1 (CFG -> PDA): 输入一个任意的CFG,输出一个等价的PDA。
- 算法2 (PDA -> CFG): 输入一个任意的PDA,输出一个等价的CFG。
- 因为我们可以进行双向的转换,所以证明了两者是等价的。
- “回想一下,我们将上下文无关语言定义为可以用上下文无关文法描述的任何语言”:
- 这是一个重要的定义回顾。CFL的“出生证明”是由CFG给出的。
- 现在,我们要证明PDA也能为CFL提供一个完全等价的“身份证明”。
- “我们的目标是以下定理”:
⚠️ [易错点]
- 证明的结构: 证明“A当且仅当B” (A <=> B),需要证明两个方向:
- A => B (如果A成立,那么B成立)
- B => A (如果B成立,那么A成立)
- 这里的证明对应:
- CFG => PDA: 如果一个语言是CFL(即有CFG),那么存在一个PDA识别它。
- PDA => CFG: 如果一个语言被PDA识别,那么它是一个CFL(即有CFG生成它)。
📝 [总结]
本段作为引言,明确了本节的宏伟目标:通过提供两个方向的转换算法,来构造性地证明上下文无关文法(CFG)和下推自动机(PDA)在语言描述能力上是等价的。它们共同定义了上下文无关语言(CFL)这一重要的语言类别。
🎯 [存在目的]
本段的目的是为读者建立一个清晰的学习预期。在深入到两个复杂的转换算法的细节之前,先让读者从宏观上理解我们要去向何方(证明等价性)以及我们将如何到达那里(通过双向构造)。这有助于读者在后续的学习中不会迷失在技术细节里。
🧠 [直觉心智模型]
这就像要证明“食谱”和“厨师”在定义“法国菜”这个概念上是等价的。
- 目标: 证明“是法国菜” <=> “能被一个法国大厨做出来”。
- 证明方向1 (食谱 -> 厨师): 我们需要展示,对于任何一本正宗的法国菜谱,我们都可以培训出一个厨师,他能且只能做出这本菜谱上的所有菜。
- 证明方向2 (厨师 -> 食谱): 我们需要展示,对于任何一个法国大厨,我们都可以通过观察和记录他的所有操作,逆向工程出一本完整的食谱,这本食谱能重现他会做的所有菜。
完成了这两个方向的证明,我们就确信“食谱”和“厨师”在定义“法国菜”的能力上是等-价的。
💭 [直观想象]
- CFG: DNA序列。它是一套编码规则,包含了制造一个完整生物体(语言)的所有信息。
- PDA: 分子机器(像核糖体)。它能读取DNA序列(输入),并根据指令(转移函数)和现有材料(栈),来合成蛋白质,并最终验证这个生物体是否符合DNA的蓝图。
本节就是要证明,任何一个DNA蓝图,都有一台对应的分子机器可以验证它;反之,任何一台这样的分子机器,我们都能反推出它所遵循的那个唯一的DNA蓝图。
4.2. 定理 2.20
📜 [原文26]
定理 2.20
一种语言是上下文无关语言当且仅当某些下推自动机识别它。
📖 [逐步解释]
这是本节要证明的核心定理,以简洁的数学语言陈述出来。
- “一种语言是上下文无关语言”:
- 根据定义,这句话等价于“存在一个上下文无关文法(CFG)可以生成该语言”。
- “当且仅当”:
- 这是逻辑上的“等价”关系,英文是 "if and only if" (iff)。
- 它意味着左右两个命题是互为充要条件的。
- 命题A 当且仅当 命题B 等价于:
- 如果A,则B (A是B的充分条件)
- 如果B,则A (A是B的必要条件)
- “某些下推自动机识别它”:
- “某些”指的是“至少存在一个”。我们不需要所有的PDA都识别它,只要能找到一个就行。
- 这句话是第二个命题。
- 定理的两个证明方向:
- 方向一 ("如果"部分, =>): 如果一个语言是上下文无关的,那么存在一个下推自动机识别它。
- 已知: 存在一个CFG $G$ 生成语言 $L$。
- 求证: 存在一个PDA $P$ 识别语言 $L$。
- 方法: 我们需要一个通用的算法,能把任意CFG $G$ 转换为等价的PDA $P$。
- 方向二 ("仅当"部分, <=): 如果存在一个下推自动机识别一个语言,那么该语言是上下文无关的。
- 已知: 存在一个PDA $P$ 识别语言 $L$。
- 求证: 存在一个CFG $G$ 生成语言 $L$。
- 方法: 我们需要一个通用的算法,能把任意PDA $P$ 转换为等价的CFG $G$。
📝 [总结]
定理2.20以形式化的语言,陈述了上下文无关语言(由CFG定义)和下推自动机(PDA)识别的语言类是完全相同的。这个“当且仅当”的论断是连接文法理论和自动机理论的关键桥梁。
🎯 [存在目的]
本定理的目的是将本节的核心论点提炼成一个精确、简洁、可引用的数学命题。在学术论证中,将主要结论表述为带编号的定理是一种标准做法,便于后续的引用、讨论和证明。
🧠 [直觉心智模型]
定理:“一个人是‘音乐家’当且仅当他能演奏至少一种乐器。”
- =>: 如果一个人是音乐家,那么他肯定会演奏至少一种乐器。
- <=: 如果一个人会演奏至少一种乐器,那么我们就可以称他为音乐家。
这个定理将“音乐家”这个抽象身份,和“能演奏乐器”这个具体能力等价了起来。定理2.20也是如此,它将“上下文无关”这个语言的抽象身份,和“能被PDA识别”这个机器的具体能力等价了起来。
4.3. 引理 2.21
📜 [原文27]
像往常一样,对于“当且仅当”定理,我们需要证明两个方向。在这个定理中,两个方向都很有趣。首先,我们做更容易的前向。
引理 2.21
如果一种语言是上下文无关语言,那么某些下推自动机识别它。
📖 [逐步解释]
这一部分开始着手证明定理2.20的第一个方向。
- “像往常一样,对于‘当且仅当’定理,我们需要证明两个方向”:
- “在这个定理中,两个方向都很有趣”:
- 暗示两个方向的证明都有其精妙之处和教学价值,值得关注。
- “首先,我们做更容易的前向”:
- 作者选择从相对简单的一半开始,即证明 CFG => PDA。
- 为什么这个方向“更容易”?因为用一个机器模型(PDA)去模拟一个基于规则的推导过程(CFG),在思路上比较直观。我们可以设计PDA的状态和栈操作来一步步地模拟文法的推导。反过来,用文法规则去描述一个机器(可能包含复杂循环和状态跳转)的所有行为,通常要困难得多。
- “引理 2.21: 如果一种语言是上下文无关语言,那么某些下推自动机识别它。”:
- 将要证明的第一个方向,被表述为一个引理(Lemma)。
- 引理通常是一个“辅助定理”,它本身是一个需要被证明的命题,但其主要目的是作为证明一个更大定理(如此处的定理2.20)的垫脚石。
- 这个引理陈述的正是“前向”的证明任务。
📝 [总结]
本段是证明的启动部分,它明确了将要先证明“前向”部分(CFG => PDA),即任何上下文无关文法都可以被转换成一个等价的下推自动机,并将其表述为引理2.21。
🎯 [存在目的]
本段的目的是清晰地划分证明的步骤,将一个大的双向证明分解为两个独立的、单向的证明任务。通过将其中的一半表述为一个引理,使得整个证明的结构更加清晰、模块化。
4.4. 证明思路
📜 [原文28]
证明思路 设 $A$ 为一个 CFL。根据定义,我们知道 $A$ 有一个生成它的 CFG,$G$。我们展示如何将 $G$ 转换为等价的 PDA,我们称之为 $P$。
我们现在描述的 PDA $P$ 将通过接受其输入 $w$ 来工作,如果 $G$ 生成该输入,则通过确定是否存在 $w$ 的推导。回想一下,推导只是文法生成字符串时所做的一系列替换。推导的每一步都产生一个变量和终结符的中间字符串。我们设计 $P$ 来确定使用 $G$ 的规则进行的一系列替换是否可以从开始变量导向 $w$。
测试是否存在 $w$ 的推导的困难之一是确定要进行哪些替换。PDA 的非确定性允许它猜测正确替换的序列。在推导的每一步,都会非确定性地选择一个特定变量的规则,并用于替换该变量。
📖 [逐步解释]
这一段是引理2.21的“证明思路”(Proof Idea)部分,它在给出严格的形式化构造之前,先用自然语言描述了核心的算法思想。
- “设 A 为一个 CFL... 有一个生成它的 CFG, G。我们展示如何将 G 转换为等价的 PDA, P”:
- 设定了证明的起点和目标。
- 起点: 一个任意给定的上下文无关文法 $G$。
- 目标: 构造一个下推自动机 $P$。
- 要求: $P$ 接受的语言必须和 $G$ 生成的语言完全相同,即 $L(P) = L(G)$。
- “PDA P 将通过...确定是否存在 w 的推导”:
- 揭示了核心策略:让PDA $P$ 模拟文法 $G$ 的推导过程。
- 推导 (Derivation): 从文法的开始变量 $S$ 开始,通过反复应用产生式规则,将变量替换为变量和终结符的字符串,直到最终得到一个只包含终结符的字符串。这个过程就是推导。
- PDA的任务: 对于一个给定的输入字符串 $w$,PDA $P$ 需要做的就是,想办法看看是否存在一条从 $S$ 出发的推导路径,其最终结果恰好是 $w$。如果能找到这样一条路径,就接受 $w$。
- “推导的每一步都产生一个变量和终结符的中间字符串”:
- 例如: $S \Rightarrow aSb \Rightarrow aaSbb \Rightarrow aabb$。这里的 $S, aSb, aaSbb, aabb$ 都是(中间)字符串。
- “我们设计 P 来确定...一系列替换是否可以从开始变量导向 w”:
- PDA $P$ 要做的,本质上是一个“搜索”问题:在所有可能的推导路径中,搜索是否存在一条路径的终点是 $w$。
- “困难之一是确定要进行哪些替换”:
- 指出了模拟推导的难点。一个变量 A 可能有多条产生式规则,比如 $A \to \alpha | \beta$。在推导的某一步,如果遇到了变量 A,我们应该用 $\alpha$ 替换它,还是用 $\beta$ 替换它呢?我们没有足够的信息来做出确定性的选择。
- “PDA 的非确定性允许它猜测正确替换的序列”:
- 给出了解决方案。这又一次体现了非确定性的威力。
- 当PDA需要替换一个变量 A 时,它不需要做出唯一的选择。相反,它会非确定性地“分裂”成多个分支,每个分支对应一条产生式规则。
- 例如,如果 $A \to \alpha$ 和 $A \to \beta$ 都是合法的规则,PDA会分裂:
- 分支1: 假设选择了 $A \to \alpha$,然后继续计算。
- 分支2: 假设选择了 $A \to \beta$,然后继续计算。
- 只要其中任何一个分支最终成功地推导出输入字符串 $w$,整个计算就算成功。
💡 [数值示例]
- 文法 G:
- $S \to aSb \mid \varepsilon$
- 输入 w: "ab"
- PDA P 的模拟过程:
- 开始: 目标是生成 "ab"。当前的中间字符串是 $S$。
- 第一步: 需要替换 $S$。有两个规则可选:$S \to aSb$ 和 $S \to \varepsilon$。PDA进行非确定性选择:
- 分支1 (选择 $S \to \varepsilon$): 中间字符串变为 $\varepsilon$。我们想得到 "ab",但现在得到了 $\varepsilon$。此路不通,死亡。
- 分支2 (选择 $S \to aSb$): 中间字符串变为 $aSb$。这看起来有希望。
- 第二步 (在分支2中继续): 当前中间字符串是 $aSb$。我们需要处理里面的变量 $S$。同样,对 $S$ 有两个规则可选:
- 分支2a (选择 $S \to aSb$): 中间字符串变为 $a(aSb)b = aaSbb$。我们的目标是 "ab",现在却走向了更长的字符串。此路不通,死亡。
- 分支2b (选择 $S \to \varepsilon$): 中间字符串变为 $a(\varepsilon)b = ab$。
- 第三步 (在分支2b中继续): 当前中间字符串是 "ab",它与输入字符串 $w$ 完全匹配,并且不含任何变量。成功!
- 结论: 因为存在一条成功的推导路径 ($S \Rightarrow aSb \Rightarrow ab$),所以PDA $P$ 会接受 "ab"。PDA通过非确定性探索,最终找到了这条路径。
⚠️ [易错点]
- 最左推导: 虽然任何推导都可以,但为了让PDA的模拟过程更有序,我们通常让它模拟“最左推导”,即在每一步总是替换最左边的那个变量。这简化了PDA的设计。
- 如何存储中间字符串: 这是下一个需要解决的关键技术问题。如果中间字符串是 $aAbB$,PDA如何访问并替换中间的变量 A 或 B?PDA只能访问栈顶。
📝 [总结]
本段提出了将CFG转换为PDA的核心思想:让PDA模拟CFG的推导过程。PDA的目标是,对于给定的输入字符串 $w$,搜索是否存在一个从开始变量到 $w$ 的推导。这个搜索过程中的选择难题,通过PDA的非确定性来完美解决——PDA可以“猜测”并同时探索所有可能的推导路径。
🎯 [存在目的]
在深入到复杂的PDA构造细节之前,本段“证明思路”的目的是为了让读者先从宏观上理解算法的逻辑和动机。一个清晰的高层思想,能极大地帮助读者理解后续那些看似繁琐的形式化步骤分别是为了实现哪个目标。
[直觉心-智模型]
这就像一个侦探在解一个密码。
- 密码本 (CFG): 包含了很多替换规则,如 A可替换为"th", B可替换为"e"等。
- 密文 (输入 w): "1221"。
- 侦探 (PDA): 他要从一个初始暗号 $S$ 开始,利用密码本的规则,看看能不能最终“推导”出 "1221"。
- 推导过程: $S \to 1A1 \to 1BBA1 \to ...$
- 非确定性: 当规则说 A 可以是 "2" 也可以是 "3" 时,侦探不需要赌一个。他可以想象两个平行宇宙,一个里面A是"2",另一个里面A是"3",然后继续推演。只要有一个宇宙能解出"1221",他就成功破案了。
📜 [原文29]
在 PDA 上实现此策略需要一个额外的想法。我们需要了解 PDA 在从一个中间字符串到另一个中间字符串时如何存储它们。简单地使用栈来存储每个中间字符串很诱人。然而,这不太奏效,因为 PDA 需要在中间字符串中找到变量并进行替换。PDA 只能访问栈顶部的符号,而这可能是一个终结符而不是一个变量。解决此问题的方法是只将中间字符串的一部分保留在栈上:从中间字符串中第一个变量开始的符号。在第一个变量之前出现的任何终结符都会立即与输入字符串中的符号匹配。下图显示了 PDA $P$。

图 2.22
表示中间字符串 01A1A0 的 $P$
📖 [逐步解释]
这一段解决了上文提出的技术难题:如何在只能访问栈顶的PDA上,存储和操作可能含有内部变量的中间推导字符串。
- “在 PDA 上实现此策略需要一个额外的想法”: 点明需要一个巧妙的设计。
- “简单地使用栈来存储每个中间字符串很诱人。然而,这不太奏效”:
- 诱人的想法: 假设中间字符串是 aAbB。一个天真的想法是把 B, b, A, a 依次压入栈,这样栈从上到下就是 a, A, b, B。
- 为什么不奏效: 如果我们要替换最左边的变量 A,我们需要先弹出 a,然后才能访问到 A。弹出 a 之后,我们还需要一个机制把它存到别处,等替换完 A 再把它放回去。这非常复杂,PDA没有这样的临时存储空间。PDA只能访问栈顶。
- “PDA 只能访问栈顶部的符号,而这可能是一个终结符而不是一个变量”:
- 以前面的例子 aAbB 为例,栈顶是 a(一个终结符)。PDA无法“跳过”a去处理下面的A。
- “解决此问题的方法是只将中间字符串的一部分保留在栈上:从中间字符串中第一个变量开始的符号”:
- 这是本节最核心、最精妙的设计思想。
- PDA的栈里,永远不存放已经匹配过的终结符,也不存放在第一个变量之前的任何终结符。
- 栈的内容 = 从推导出的中间字符串的“最左变量”开始,到字符串末尾的全部内容。
- “在第一个变量之前出现的任何终结符都会立即与输入字符串中的符号匹配”:
- 这是对上述思想的补充。如果推导出 aAbB,而最左变量是 A,那么 A 前面的 a 怎么办?
- 答案是:不把它放进栈里。而是立刻用它去和输入字符串 $w$ 的当前符号进行匹配。如果匹配成功,就消耗掉这个输入符号,然后继续处理 AbB。如果匹配失败,这条计算路径就死亡。
- 分析图 2.22:
- 图示:
- 输入带 (Input): 01...
- PDA的栈 (Stack): 从上到下是 A, 1, A, 0, $。
- 表示的中间字符串: 01A1A0。
- 如何对应:
- 输入带的指针已经越过了 "01"。这说明 "01" 已经被PDA处理并与推导出的终结符成功匹配了。
- 中间字符串 01A1A0 中,最左边的变量是 A。
- 根据规则,PDA的栈里应该存放从这个A开始到末尾的字符串,即 A1A0。
- 为了方便压栈,通常是反过来压,所以栈从底向上是 $, 0, A, 1, A。栈顶是 A。
- PDA的下一步操作:
- PDA看到栈顶是变量 A。
- 它会非确定性地选择一个 A 的产生式规则,比如 A -> t1 t2。
- 然后它会弹出 A,并把 t1 t2 的反转 t2 t1 压入栈中。
- 这样,新的栈顶就是 t1,正好是新推导出的字符串片段的第一个符号,PDA可以继续处理它了。
💡 [数值示例]
- 文法 G: $S \to 0S1 \mid \varepsilon$
- 输入 w: "0011"
- PDA 模拟最左推导:
- 初始:
- 中间串: $S$
- 栈: [S, $]
- 输入指针: ^0011
- 步骤1: 栈顶是变量 $S$。非确定性选择 $S \to 0S1$。
- 弹出 $S$,把 0S1 (反转为 1S0) 压入栈。
- 中间串(逻辑上): 0S1
- 栈: [0, S, 1, $] (栈顶是0)
- 输入指针: ^0011
- 步骤2: 栈顶是终结符 '0'。
- 匹配输入。输入指针处的符号也是 '0'。匹配成功!
- 弹出 '0',输入指针右移。
- 栈: [S, 1, $]
- 输入指针: 0^011
- 步骤3: 栈顶是变量 $S$。选择 $S \to 0S1$。
- 弹出 $S$,压入 1S0。
- 栈: [0, S, 1, 1, $] (栈顶是0)
- 输入指针: 0^011
- 步骤4: 栈顶是终结符 '0'。
- 匹配输入 '0'。成功!
- 弹出 '0',输入指针右移。
- 栈: [S, 1, 1, $]
- 输入指针: 00^11
- 步骤5: 栈顶是变量 $S$。选择 $S \to \varepsilon$。
- 弹出 $S$,不压入任何东西。
- 栈: [1, 1, $]
- 输入指针: 00^11
- 步骤6: 栈顶是终结符 '1'。
- 匹配输入 '1'。成功!
- 弹出 '1',输入指针右移。
- 栈: [1, $]
- 输入指针: 001^1
- 步骤7: 栈顶是终结符 '1'。
- 匹配输入 '1'。成功!
- 弹出 '1',输入指针右移。
- 栈: [$]
- 输入指针: 0011^
- 步骤8: 栈顶是栈底符号 $。
- 这表示推导出的终结符串和输入字符串完全匹配。
- 弹出 $,进入接受状态。成功!
⚠️ [易错点]
- 反转压栈: 当用产生式的右侧 uvw 替换变量时,必须将 w, v, u 依次压栈,以确保 u 在最顶上,维持了“最左推导”的顺序。
- 终结符处理: 栈顶只要是终结符,PDA的唯一工作就是匹配输入,不能做其他选择。匹配失败则路径死亡。
📝 [总结]
本段提出了一个将CFG转换为PDA的精妙实现策略。PDA的栈并不存储完整的中间推导字符串,而只存储从“最左变量”开始到末尾的部分。任何出现在最左变量之前的终结符,都会被立即用于匹配输入字符串。这个设计完美地结合了PDA只能访问栈顶的特性和模拟最左推导的需求。
🎯 [存在目的]
本段的目的是解决CFG到PDA转换中的核心技术难题。它提供了一个具体、可行的算法,说明了如何用一个受限的机器模型(PDA)来模拟一个看似更灵活的推导过程。这是从“证明思路”到“可实现算法”的关键一步。
🧠 [直觉心智模型]
这个PDA就像一个“目标驱动”的解析器。
- 栈: 你的“待办事项”清单。清单上的任务是从上到下处理。
- 初始状态: 你的待办清单上只有一项任务:“生成目标字符串 w”。我们把起始符号 S 放到清单顶部。
- 工作循环:
- 如果清单顶部是“生成A” (变量): 你查阅规则手册,发现A可以由 uvw 构成。你把“生成A”划掉,换成三个新任务,按顺序是“生成u”、“生成v”、“生成w”,把它们放到清单顶部。
- 如果清单顶部是“匹配a” (终结符): 你看看你的输入流,下一个是不是'a'。如果是,太好了,你划掉这个任务,并消耗掉输入'a'。如果不是,这条路走不通了。
- 这个过程一直持续,直到你的待办清单为空,并且输入流也正好用完。
4.5. PDA的非正式描述
📜 [原文30]
以下是 $P$ 的非正式描述。
- 将标记符号 $\$$ 和开始变量放在栈上。
- 永远重复以下步骤。
a. 如果栈顶是变量符号 $A$,则非确定性地选择 $A$ 的规则之一,并用规则右侧的字符串替换 $A$。
b. 如果栈顶是终结符 $a$,则从输入中读取下一个符号并将其与 $a$ 比较。如果它们匹配,则重复。如果它们不匹配,则在此非确定性分支上拒绝。
c. 如果栈顶是符号 $\$$,则进入接受状态。如果输入已全部读取,则接受输入。
📖 [逐步解释]
这一段将前面提出的CFG到PDA转换策略,总结成一个清晰的、三步走的非正式算法。
- “1. 将标记符号 \$ 和开始变量放在栈上”:
- 这是PDA的初始化步骤。
- $: 栈底标记,用于判断栈何时被清空。
- 开始变量 (通常是 S): 这是推导的起点。PDA的初始“待办事项”就是处理开始变量 S。
- 栈内容将是 [S, $](S在栈顶)。
- “2. 永远重复以下步骤”:
- 这是一个主循环,PDA会一直执行,直到接受或所有路径都拒绝。
- “a. 如果栈顶是变量符号 A ...”:
- 这是处理变量的规则。
- 触发条件: 栈顶是一个变量。
- 动作:
- 非确定性选择: 查看文法 $G$ 中所有以 $A$ 为左侧的产生式规则,例如 $A \to \alpha_1, A \to \alpha_2, \dots$。
- 替换: 对于每个规则 $A \to \alpha_i$,PDA都可能分裂出一个分支。在该分支中,它首先弹出 A,然后将 $\alpha_i$ 的反转形式压入栈中。例如,如果规则是 $A \to XYZ$,它就压入 Z,再压入 Y,再压入 X。
- “b. 如果栈顶是终结符 a ...”:
- 这是处理终结符的规则。
- 触发条件: 栈顶是一个终结符。
- 动作:
- 读取输入: 查看输入带上磁头当前指向的符号。
- 比较: 将输入符号与栈顶终结符 $a$ 进行比较。
- 如果匹配: 太好了!弹出 $a$,并将输入磁头向右移动一格。循环继续。
- 如果不匹配: 这条计算路径是错误的。PDA在该分支上失败并终止(拒绝)。
- “c. 如果栈顶是符号 \$ ...”:
- 这是计算成功的判断规则。
- 触发条件: 栈顶是栈底标记 $。
- 这意味着: 所有从开始变量 $S$ 推导出来的、需要匹配的符号(无论是变量还是终结符)都已经被成功处理掉了。
- 动作:
- 进入接受状态: PDA转移到一个特殊的接受状态。
- 检查输入: 接受的最终条件是,此时此刻,输入字符串也必须被完全读取。如果输入带上还有剩余符号,那么即使栈空了,也意味着推导出的字符串比输入短,匹配失败,拒绝。如果输入和栈同时“清空”,则接受。
💡 [数值示例]
- 文法 G: $S \to 0S1 \mid \varepsilon$
- 输入 w: "01"
- PDA 运行轨迹:
- 步骤1 (初始化): 栈 [S, $]。输入 ^01。
- 步骤2.a (处理变量S): 栈顶是 $S$。非确定性选择 $S \to 0S1$。
- 弹出 $S$,压入 1,再压入 S,再压入 0。
- 栈: [0, S, 1, $]。输入 ^01。
- 步骤2.b (处理终结符0): 栈顶是 '0'。输入也是 '0'。匹配!
- 弹出 '0'。输入磁头右移。
- 栈: [S, 1, $]。输入 0^1。
- 步骤2.a (处理变量S): 栈顶是 $S$。非确定性选择 $S \to \varepsilon$。
- 弹出 $S$。不压入任何东西。
- 栈: [1, $]。输入 0^1。
- 步骤2.b (处理终结符1): 栈顶是 '1'。输入也是 '1'。匹配!
- 弹出 '1'。输入磁头右移。
- 栈: [$]。输入 01^。
- **步骤2.c (处理栈底$)**: **栈顶**是 `$`。此时输入也已全部读取。
⚠️ [易错点]
- 拒绝条件: 这个描述主要说明了如何走向接受。拒绝发生在:
- 步骤2.b中,终结符不匹配。
- 计算卡住:对于当前的 (状态, 输入, 栈顶) 组合,没有任何合法的转移。
- 所有非确定性分支都走到了拒绝状态。
- 空字符串 $\varepsilon$: 如果文法可以生成 $\varepsilon$ (比如 $S \to \varepsilon$),那么PDA在第一步初始化后,栈顶是 $S$。它可以选择 $S \to \varepsilon$ 这条规则,弹出 $S$,栈变为 `[$]`。此时如果输入也是 $\varepsilon$,则根据步骤2.c,接受。
📝 [总结]
本段提供了一个将任意CFG转换为等价PDA的高层算法。该PDA通过一个主循环来模拟最左推导:
- 初始化: 将开始变量 $S$ 和栈底 $ 压栈。
- 循环:
- 若栈顶是变量,则非确定性地应用一条产生式规则进行替换(弹旧压新)。
- 若栈顶是终结符,则与输入进行匹配(匹配则弹,不匹配则拒绝)。
- 若栈顶是栈底 $,并且输入也已耗尽,则接受。
🎯 [存在目的]
本段的目的是将之前分散的思路和例子,整合成一个统一、简洁且易于理解的算法描述。这个非正式的描述是形式化构造之前的重要一步,它让读者能够脱离繁琐的数学符号,专注于算法的核心逻辑。
[直觉心-智模型]
这就像一个递归下降的解析器。
- 栈: 函数调用的调用栈。
- 处理变量 A: 调用一个名为 parse_A() 的函数。
- parse_A() 函数内部: 根据文法规则 $A \to \alpha | \beta$,它会尝试调用 parse_alpha(),如果失败了(在非确定性模型中是并行尝试),就尝试 parse_beta()。
- 处理终结符 a: 调用一个名为 match('a') 的函数,它会检查下一个输入是不是 'a'。
- 栈底 $: 相当于主函数 main() 的返回地址。当所有解析任务都完成,返回到 main 时,计算成功。
55. 实现与证明
5.1. 引理2.21的证明
📜 [原文31]
证明 我们现在给出下推自动机 $P=\left(Q, \Sigma, \Gamma, \delta, q_{\text {start }}, F\right)$ 构造的形式细节。为了使构造更清晰,我们使用转移函数的简写表示法。这种表示法提供了一种在机器的一个步骤中将整个字符串写入栈的方法。我们可以通过引入额外的状态来模拟此操作,以便一次写入一个符号,如下面的形式构造所示。
设 $q$ 和 $r$ 是 PDA 的状态,设 $a$ 在 $\Sigma_{\varepsilon}$ 中,设 $s$ 在 $\Gamma_{\varepsilon}$ 中。假设我们希望 PDA 在读取 $a$ 并弹栈 $s$ 时从 $q$ 转移到 $r$。此外,我们希望它同时将整个字符串 $u=u_{1} \cdots u_{l}$ 压栈。我们可以通过引入新状态 $q_{1}, \ldots, q_{l-1}$ 并按如下方式设置转移函数来实施此操作:
$$
\begin{aligned}
& \delta(q, a, s) \text { 包含 }\left(q_{1}, u_{l}\right), \\
& \delta\left(q_{1}, \varepsilon, \varepsilon\right)=\left\{\left(q_{2}, u_{l-1}\right)\right\}, \\
& \delta\left(q_{2}, \varepsilon, \varepsilon\right)=\left\{\left(q_{3}, u_{l-2}\right)\right\}, \\
& \vdots \\
& \delta\left(q_{l-1}, \varepsilon, \varepsilon\right)=\left\{\left(r, u_{1}\right)\right\} .
\end{aligned}
$$
我们使用符号 $(r, u) \in \delta(q, a, s)$ 来表示当 $q$ 是自动机的状态,$a$ 是下一个输入符号,$s$ 是栈顶符号时,PDA 可以读取 $a$ 并弹栈 $s$,然后将字符串 $u$ 压栈并进入状态 $r$。下图显示了此实现。

图 2.23
实现简写 $(r, x y z) \in \delta(q, a, s)$
📖 [逐步解释]
这一部分进入了引理2.21的正式证明,首先通过引入一个方便的“简写表示法”来简化后续的构造描述。
- “我们现在给出...形式细节”: 表明从高层思路转向底层实现。
- “为了使构造更清晰,我们使用转移函数的简写表示法”:
- 标准的PDA每一步只能压入一个栈符号(或$\varepsilon$)。
- 但在模拟CFG时,我们需要压入一个产生式右侧的整个字符串,比如 XYZ。
- 为了方便描述,我们想有一个“宏指令”,能让我们说“一次性把 XYZ 压栈”。这个简写表示法就是这个宏指令。
- “我们可以通过引入额外的状态来模拟此操作”:
- 这里解释了这个“宏指令”如何用标准的PDA操作来实现。它不是PDA的新功能,只是一个语法糖。
- 思想: 将一次性的多符号 压栈,分解成连续的、多次的单符号 压栈。
- 如何实现:
- 假设要压入 u = u1 u2 ... ul。
- 第一步:执行原始的输入读取和弹栈,然后压入 ul (字符串的最后一个符号),并进入一个临时的中间状态 $q_1$。
- 第二步:从 $q_1$ 出发,进行一次 $\varepsilon$-转移,不读输入,不弹栈,只压入 u(l-1),并进入下一个临时状态 $q_2$。
- ...
- 第 $l$ 步:从 $q_{l-1}$ 出发,进行 $\varepsilon$-转移,压入 u1 (字符串的第一个符号),并进入最终的目标状态 $r$。
- 这样,通过 $l-1$ 个中间状态和 $l$ 步转移,就实现了将整个字符串 u(以反转的顺序)压入栈中,并确保最终 u1 在栈顶。
- 形式化描述和图2.23:
- 公式: 精确地给出了上述 $l$ 步的转移函数设置。
- $\delta(q, a, s)$ 包含 $(q_1, u_l)$: 这是第一步,处理输入 $a$ 和原栈顶 $s$,压入 u 的最后一个符号 u_l。
- $\delta(q_{i}, \varepsilon, \varepsilon) = ...$: 后续的所有步骤都是 $\varepsilon$-转移,不看输入也不看栈,只是顺序地压入 u 的剩余部分。
- 图2.23: 直观地画出了这个过程。对于简写 a, s -> xyz,它被分解成三步:
- 从 q 到 q1: a, s -> z
- 从 q1 到 q2: ε, ε -> y
- 从 q2 到 r: ε, ε -> x
- “我们使用符号 $(r, u) \in \delta(q, a, s)$ 来表示...”:
- 正式定义了这个简写。以后我们看到 `$(r, u)` 出现在**转移函数**的输出**集合**中,就理解为“转移到**状态** $r$,并将整个**字符串** $u$ 压入栈中”。我们心里清楚,这背后是一连串的单符号 压栈操作。
📝 [总结]
本段为了简化后续从CFG到PDA的构造证明,引入并定义了一个方便的简写表示法:$(r, u) \in \delta(q, a, s)$,意为“转移到状态 $r$ 并将整个字符串 $u$ 压入栈”。同时,本段也证明了这个简写是合法的,因为它可以用标准的PDA操作(通过引入中间状态和一系列$\varepsilon$-转移)来模拟实现。
🎯 [存在目的]
本段的目的是“磨刀不误砍柴工”。通过先定义好一个强大的、方便的工具(简写表示法),可以使后续真正核心的构造过程描述起来更加简洁、清晰,让读者能专注于算法逻辑本身,而不会被繁琐的单步压栈细节所淹没。
[直觉心-智模型]
这就像在编程中定义一个函数或宏。
- 标准PDA操作: 就像是基础的汇编指令,比如 MOV, ADD。
- 我们的目标: 需要一个 PUSH_STRING("XYZ") 的高级指令。
- 本段的工作: 就是用基础的汇编指令,编写一个名为 PUSH_STRING 的函数。
```assembly
PUSH_STRING(str):
PUSH str[last]
PUSH str[last-1]
...
PUSH str[first]
```
- 定义好这个函数后,以后我们在主程序里就可以直接调用 PUSH_STRING("XYZ"),而不需要每次都手写那一长串的 PUSH 指令了。
📜 [原文32]
$P$ 的状态是 $Q=\left\{q_{\text {start }}, q_{\text {loop }}, q_{\text {accept }}\right\} \cup E$,其中 $E$ 是我们为实现刚刚描述的简写而需要的状态集。开始状态是 $q_{\text {start}}$。唯一的接受状态是 $q_{\text {accept}}$。
转移函数定义如下。我们首先将栈初始化为包含符号 $\$$ 和 $S$,实现非正式描述中的步骤 1:$\delta\left(q_{\text {start }}, \boldsymbol{\varepsilon}, \boldsymbol{\varepsilon}\right)=\left\{\left(q_{\text {loop }}, S \$\right)\right\}$。然后我们为步骤 2 的主循环放入转移。
首先,我们处理情况 (a),其中栈顶包含一个变量。设 $\delta\left(q_{\text {loop }}, \boldsymbol{\varepsilon}, A\right)=\left\{\left(q_{\text {loop }}, w\right) \mid \text { 其中 } A \rightarrow w \text { 是 } R \text { 中的一条**规则** }\right\}$。
其次,我们处理情况 (b),其中栈顶包含一个终结符。设 $\delta\left(q_{\text {loop }}, a, a\right)=\left\{\left(q_{\text {loop }}, \varepsilon\right)\right\}$。
最后,我们处理情况 (c),其中空栈标记 $\$$ 在栈顶。设 $\delta\left(q_{\text {loop }}, \boldsymbol{\varepsilon}, \$\right)=\left\{\left(q_{\text {accept }}, \boldsymbol{\varepsilon}\right)\right\}$。
状态图如图 2.24 所示。

图 2.24
$P$ 的状态图
📖 [逐步解释]
这一部分给出了将CFG转换为PDA的正式构造算法。这个构造的PDA $P$ 只有三个主要状态,逻辑非常清晰。
- 定义 $P$ 的组件:
- 状态集 $Q$:
- $
- $q_{\text{loop}}$: 一个核心的“主循环”状态。所有的推导和匹配工作都在这个状态的自环上完成。
- $q_{\text{accept}}$: 唯一的接受状态。
- $E$: 一个附加的状态集合,用于实现上一段提到的“压入整个字符串”的简写。这些是幕后英雄,在我们的高层逻辑中是透明的。
- 开始状态: $q_{\text{start}}$。
- 接受状态集 F: $\{q_{\text{accept}}\}$。
- 定义转移函数 $\delta$:
- 初始化: $\delta\left(q_{\text {start }}, \boldsymbol{\varepsilon}, \boldsymbol{\varepsilon}\right)=\left\{\left(q_{\text {loop }}, S \$\right)\right\}$
- 触发: 在开始状态 $q_{\text{start}}$,无条件地(输入$\varepsilon$,栈顶$\varepsilon$)。
- 动作: 转移到主循环状态 $q_{\text{loop}}$,并(使用简写)将开始变量 $S$ 和栈底标记 $` **压入栈**中。由于是简写,实际是先**压**`$,再压S,所以栈变为 [S, $]。
- 目的: 完美实现了非正式描述的步骤1。
- 主循环 - 情况 (a) 栈顶是变量:
- $\delta\left(q_{\text {loop }}, \boldsymbol{\varepsilon}, A\right)=\left\{\left(q_{\text {loop }}, w\right) \mid \text { 其中 } A \rightarrow w \text { 是 } G \text { 中的一条**规则** }\right\}$
- 触发: 在 $q_{\text{loop}}$ 状态,不读输入($\varepsilon$),当栈顶是任何一个变量 $A$ 时。
- 动作: 非确定性地,对于每一条关于 $A$ 的产生式规则 $A \to w$,都存在一个转移:保持在 $q_{\text{loop}}$ 状态,弹出 $A$,并(使用简写)将字符串 $w$ 压入栈中。
- 目的: 实现了非正式描述的步骤2.a,即模拟推导。
- 主循环 - 情况 (b) 栈顶是终结符:
- $\delta\left(q_{\text {loop }}, a, a\right)=\left\{\left(q_{\text {loop }}, \varepsilon\right)\right\}$
- 触发: 在 $q_{\text{loop}}$ 状态,当输入符号 $a$ 和栈顶符号 $a$ 相同时。
- 动作: 保持在 $q_{\text{loop}}$ 状态,弹出 $a$(通过压入$\varepsilon$实现),输入磁头前进一格。
- 目的: 实现了非正式描述的步骤2.b,即匹配终结符。
- 主循环 - 情况 (c) 栈顶是栈底标记:
- $\delta\left(q_{\text {loop }}, \boldsymbol{\varepsilon}, \$\right)=\left\{\left(q_{\text {accept }}, \boldsymbol{\varepsilon}\right)\right\}$
- 触发: 在 $q_{\text{loop}}$ 状态,不读输入,当栈顶是栈底标记 $ 时。
- 动作: 转移到唯一的接受状态 $q_{\text{accept}}$,并弹出 $。
- 目的: 实现了非正式描述的步骤2.c,即成功结束计算。
- 分析图 2.24:
- 这个状态图完美地可视化了上述构造。
- $q_{\text{start}} \to q_{\text{loop}}$: 初始化的 $\varepsilon, \varepsilon \to S\$` 转移。
- $q_{\text{loop}}$ 的自环:
- $\varepsilon, A \to w$ (for each rule A->w): 这是情况(a),模拟推导。
- $a, a \to \varepsilon$ (for each terminal a): 这是情况(b),匹配终结符。
- $q_{\text{loop}} \to q_{\text{accept}}$: 结束的 $\varepsilon, \$ \to \varepsilon$ 转移。
📝 [总结]
本段给出了从任意CFG构造等价PDA的完整、形式化的算法。该PDA ($P$) 结构极其简洁,只用了三个主要状态(开始、循环、接受)。
- 它首先通过一次转移完成初始化,进入循环状态 $q_{\text{loop}}$。
- 在 $q_{\text{loop}}$ 中,它通过自环,根据栈顶是变量还是终结符,来分别执行“推导”或“匹配”操作。
- 当所有推导和匹配完成(表现为栈顶出现栈底标记),它就从 $q_{\text{loop}}$ 退出,进入接受状态。
这个优雅的构造证明了引理2.21。
🎯 [存在目的]
本段的目的是将CFG到PDA的转换思想,从非正式的算法描述,落实为严格的、数学上无懈可击的PDA构造。它为引理2.21提供了一个普适的、构造性的证明。任何一个CFG,都可以像套公式一样代入这个流程,生成一个对应的PDA。
🧠 [直觉心智模型]
这个构造出的PDA就像一个只有一个主工作台的工厂。
- $q_{\text{start}}$ (大门口): 货物(输入)和订单(CFG)进来,工人(PDA)拿到初始任务(开始变量 S),进入车间。
- $q_{\text{loop}}$ (主工作台): 工人永远待在这里。他只看工作台最上面的任务单(栈顶)。
- 任务是“组装A” (变量): 他查阅手册(CFG规则),把任务A换成一叠新的子任务,放到工作台顶上。
- 任务是“检验零件a” (终结符): 他从传送带(输入)上拿一个零件,看是不是a。是就扔掉,继续看下一个任务;不是就整条生产线报警(拒绝)。
- $q_{\text{accept}}$ (成品仓库): 当工人发现工作台上的任务清单只剩一张“初始标记”时,他知道所有工作都做完了,于是把成品送入仓库(接受)。
5.2. 例子 2.25
📜 [原文33]
我们使用引理 2.21 中开发的程序从以下 CFG $G$ 构造一个 PDA $P_{1}$。
$$
\begin{aligned}
& S \rightarrow \mathrm{a} T \mathrm{~b} \mid \mathrm{b} \\
& T \rightarrow T \mathrm{a} \mid \varepsilon
\end{aligned}
$$
转移函数如下图所示。

图 2.26
$P_{1}$ 的状态图
📖 [逐步解释]
这个例子应用了刚刚学习的构造算法,将一个给定的CFG转换成一个PDA。
- 分析给定的 CFG G:
- $S \rightarrow \mathrm{a} T \mathrm{~b} \mid \mathrm{b}$: 开始变量 $S$ 可以生成 aTb 或者直接生成 b。
- $T \rightarrow T \mathrm{a} \mid \varepsilon$: 变量 $T$ 可以生成自身加上一个'a'(形成Taaaa...),或者生成空串 $\varepsilon$。
- 这个文法生成什么语言?:
- 从 $T$ 开始,通过反复应用 $T \to Ta$,可以得到 $T \to T a^n$。最后应用 $T \to \varepsilon$,可以得到 $T \Rightarrow^* a^n$。所以 $T$ 可以生成任意数量的'a'(包括0个)。
- 再看 $S$。如果走 $S \to b$,就生成字符串 "b"。
- 如果走 $S \to aTb$,并且 $T \Rightarrow^* a^n$,那么 $S \Rightarrow a(a^n)b = a^{n+1}b$。
- 所以,该语言 $L(G) = \{a^k b \mid k \ge 1\} \cup \{b\}$,可以简写为 $\{a^k b \mid k \ge 0\}$。
- 应用构造算法生成 PDA P1:
- 根据算法,PDA有三个主要状态: $q_{\text{start}}, q_{\text{loop}}, q_{\text{accept}}$。
- 转移规则如下:
- 初始化: 从 $q_{\text{start}}$ 到 $q_{\text{loop}}$ 有一条转移 $\varepsilon, \varepsilon \to S\$。
- 处理变量 (在 $q_{\text{loop}}$):
- 栈顶是 $S$:
- 对于规则 $S \to aTb$: 有转移 $\varepsilon, S \to aTb$。
- 对于规则 $S \to b$: 有转移 $\varepsilon, S \to b$。
- 栈顶是 $T$:
- 对于规则 $T \to Ta$: 有转移 $\varepsilon, T \to Ta$。
- 对于规则 $T \to \varepsilon$: 有转移 $\varepsilon, T \to \varepsilon$。
- 处理终结符 (在 $q_{\text{loop}}$):
- 输入是 'a',栈顶是 'a': 有转移 a, a -> ε。
- 输入是 'b',栈顶是 'b': 有转移 b, b -> ε。
- 结束:
- 栈顶是 '$': 有**转移**从 $q_{\text{loop}}$ 到 $q_{\text{accept}}$,`$\varepsilon, \$ \to \varepsilon$`。
- 分析图 2.26 (P1的状态图):
- 这张图完全就是将上面列出的转移规则画了出来。
- $q_{start} \to q_{loop}$: $\varepsilon, \varepsilon \to S\$` (初始化)。
- $q_{loop}$ 的自环:
- $\varepsilon, S \to aTb$
- $\varepsilon, S \to b$
- $\varepsilon, T \to Ta$
- $\varepsilon, T \to \varepsilon$
- 这四条都是模拟推导的。
- a, a -> ε
- b, b -> ε
- 这两条是匹配终结符的。
- $q_{loop} \to q_{accept}}$: $\varepsilon, \$ \to \varepsilon$ (成功退出)。
- 这个状态图是上一节通用构造图(图2.24)的一个具体实例。
💡 [数值示例]
- 初始: (q_start, aab, ε) -> 走初始化转移 -> (q_loop, aab, S$)。
- 栈顶S: 选 S->aTb。弹出S, 压入b,T,a。 -> (q_loop, aab, aTb$)。
- 栈顶a: 匹配输入'a'。弹出a, 输入指针右移。 -> (q_loop, ab, Tb$)。
- 栈顶T: 选 T->Ta。弹出T, 压入a,T。 -> (q_loop, ab, aTb$)。
- 栈顶a: 匹配输入'a'。弹出a, 输入指针右移。 -> (q_loop, b, Tb$)。
- 栈顶T: 选 T->ε。弹出T。 -> (q_loop, b, b$)。
- 栈顶b: 匹配输入'b'。弹出b, 输入指针右移。 -> (q_loop, ε, $)。
- 栈顶$: 输入结束。走退出转移。 -> (q_accept, ε, ε)。
- 接受。
📝 [总结]
本例演示了如何将引理2.21的通用构造程序应用于一个具体的上下文无关文法 $G$。通过按部就班地将 $G$ 的产生式规则和终结符转化为PDA $P_1$ 在主循环状态 $q_{\text{loop}}$ 上的转移,我们得到了一个与 $G$ 等价的PDA的状态图。
🎯 [存在目的]
本例的目的是通过一个实践操作,来巩固读者对CFG到PDA转换算法的理解。理论算法往往是抽象的,一个具体的例子能让读者看到算法的每一步是如何落实的,从而将抽象知识转化为可操作的技能。
5.3. 引理 2.27
📜 [原文34]
现在我们证明定理 2.20 的反向。对于前向,我们给出了将 CFG 转换为 PDA 的过程。主要思想是设计自动机以模拟文法。现在我们想提供一个相反方向的过程:将 PDA 转换为 CFG。我们设计文法以模拟自动机。这项任务具有挑战性,因为“编程”自动机比“编程”文法更容易。
引理 2.27
如果下推自动机识别某种语言,那么它是上下文无关语言。
📖 [逐步解释]
这一部分开始进行定理2.20的后半部分证明,即 PDA => CFG。
- “现在我们证明定理 2.20 的反向”: 明确指出证明任务的转换。
- “主要思想是设计自动机以模拟文法。现在我们想...设计文法以模拟自动机”:
- 这是一个非常精彩的对称性描述。
- 前向 (CFG->PDA): 我们让机器(PDA)的行为去模仿规则(CFG)。
- 后向 (PDA->CFG): 我们要用规则(CFG)去描述机器(PDA)的所有可能行为。
- “这项任务具有挑战性,因为‘编程’自动机比‘编程’文法更容易”:
- 作者坦言这个方向的证明更难。
- “编程”自动机:我们思考的是一步步的流程、状态转换、条件判断,这更接近我们常规的算法思维。
- “编程”文法: 我们思考的是如何用递归的、声明式的规则来生成一个集合。这通常更抽象,更考验数学构造能力。要用文法规则去捕捉一个机器所有可能的、包含循环和分支的计算路径,是一件非常棘手的事情。
- “引理 2.27: 如果下推自动机识别某种语言,那么它是上下文无关语言”:
- 将后向证明任务表述为引理2.27。
- 已知: 存在一个PDA $P$ 识别语言 $L$。
- 求证: $L$ 是一个上下文无关语言,即存在一个CFG $G$ 使得 $L(G) = L(P)$。
📝 [总结]
本段引入了定理2.20证明的第二部分:将任意PDA转换为等价的CFG。作者指出了这个方向的挑战性所在(用文法模拟机器比用机器模拟文法更难),并以引理2.27的形式正式提出了这个证明任务。
🎯 [存在目的]
本段的作用是承上启下,结束前一个证明,开启下一个更具挑战性的证明。通过坦言其难度,可以引起读者的重视,并为后续出现更复杂的构造思想做好心理准备。
5.4. 证明思路
📜 [原文35]
证明思路 我们有一个 PDA $P$,我们想创建一个 CFG $G$,它生成所有 $P$ 接受的字符串。换句话说,$G$ 应该生成一个字符串,如果该字符串导致 PDA 从其开始状态转移到接受状态。
为了达到这个结果,我们设计了一个做更多事情的文法。对于 $P$ 中每对状态 $p$ 和 $q$,文法将有一个变量 $A_{p q}$。这个变量生成所有可以将 $P$ 从 $p$(空栈)带到 $q$(空栈)的字符串。请注意,这样的字符串也可以将 $P$ 从 $p$ 带到 $q$,无论 $p$ 处的栈内容如何,并在 $q$ 处使栈与 $p$ 处的状态相同。
首先,我们通过稍微修改 $P$ 来简化我们的任务,使其具有以下三个特征。
- 它有一个单一的接受状态,$q_{\text {accept}}$。
- 它在接受前清空其栈。
- 每个转移要么将符号压栈(压栈操作),要么从栈中弹栈(弹栈操作),但不同时进行两者。
为了设计 $G$ 以使 $A_{p q}$ 生成所有以空栈从 $p$ 到 $q$ 的字符串,我们必须了解 $P$ 如何对这些字符串进行操作。对于任何这样的字符串 $x$, $P$ 在 $x$ 上的第一个动作必须是压栈,因为每个动作要么是压栈要么是弹栈,并且 $P$ 不能弹栈一个空栈。同样,在 $x$ 上的最后一个动作必须是弹栈,因为栈最终会变空。
在 $P$ 对 $x$ 的计算过程中会出现两种可能性。要么最后弹栈的符号与最初压栈的符号相同,要么不同。如果是这样,栈可能只在 $P$ 对 $x$ 的计算的开始和结束时为空。如果不是,最初压栈的符号必须在 $x$ 结束之前的某个点被弹栈,因此栈在此点变空。我们用规则 $A_{p q} \rightarrow a A_{r s} b$ 模拟前一种可能性,其中 $a$ 是第一个动作读取的输入,$b$ 是最后一个动作读取的输入,$r$ 是 $p$ 之后的状态,$s$ 是 $q$ 之前的状态。我们用规则 $A_{p q} \rightarrow A_{p r} A_{r q}$ 模拟后一种可能性,其中 $r$ 是栈变空时的状态。
📖 [逐步解释]
这一段是引理2.27的“证明思路”,它描述了将PDA转换为CFG的非常精妙和核心的思想。
- 总体目标: 给定PDA $P$,创建CFG $G$,使得 $G$ 能生成所有 $P$ 接受的字符串。
- 核心思想 - 引入特殊变量:
- 直接模拟PDA的完整计算(包含栈的变化)太复杂了,无法用CFG规则表达。
- 巧妙的转换: 我们不直接模拟计算,而是定义一种特殊的变量,让这个变量代表一种特殊的计算片段。
- 变量 $A_{pq}$: 这个变量代表一个“子任务”:生成所有能让PDA $P$ 从状态 $p$ 走到状态 $q$,并且在这个过程中,栈的变化是“净零”的字符串。
- “从 p (空栈) 带到 q (空栈)”: 这是对“净零”变化的最严格定义。它意味着计算开始时栈是空的,结束时栈也必须是空的。
- 推广: 如果计算开始时栈不是空的,比如是 T,那么结束时栈也必须是 T。也就是说,这个计算片段消耗掉了它自己产生的所有栈内容,但没有动它下面的东西。
- 简化PDA (预处理):
- 在开始构造文法前,先对给定的任意PDA $P$ 做一些标准化改造,得到一个行为等价但形式更简单的 $P'$。这会大大简化后续文法规则的构造。
- 特征1: 单一接受状态: 如果原PDA有多个接受状态,可以新建一个唯一的最终接受状态 $q_{\text{accept}}$,然后从所有原接受状态画一条 $\varepsilon$-转移到这个新状态。
- 特征2: 接受前清空栈: 在到达新的唯一接受状态 $q_{\text{accept}}$ 之前,让PDA进入一个循环,弹出所有栈中剩余的符号,直到栈空,然后再进入 $q_{\text{accept}}$。
- 特征3: 每次转移只做一件事:
- 将 a, X -> Y (替换) 分解为两步: a, X -> ε (弹栈) -> ε, ε -> Y (压栈)。这需要一个中间状态。
- 将 a, ε -> ε (只读输入) 分解为两步: a, ε -> Z (压栈) -> ε, Z -> ε (弹栈)。Z 是一个任意的临时栈符号。
- 这些改造都可以通过增加一些状态和 $\varepsilon$-转移来完成,不会改变PDA识别的语言。但改造后的PDA行为变得非常规整:每一步要么纯压栈,要么纯弹栈。
- 构造文法规则 (核心逻辑):
- 现在我们用这个简化后的PDA来构造文法 $G$。目标是为每个变量 $A_{pq}$ 找到合适的产生式规则。
- 考虑一个能让PDA从 $p$ (空栈) 走到 $q$ (空栈) 的字符串 $x$。
- 第一个动作: 必然是压栈。因为栈是空的,不能弹。设这次压栈发生在读入符号 $a$ 时,压入了符号 $u$,进入了状态 $r$。
- 最后一个动作: 必然是弹栈。因为栈最后变回空,所以最后一步必须是消耗掉某个符号。设这次弹栈发生在读入符号 $b$ 时,弹出了符号 $v$,从状态 $s$ 进入了状态 $q$。
- 两种可能性:
- 情况1: “配对”的计算: 第一个压入的符号 $u$ 和最后一个弹出的符号 $v$ 是同一个。这意味着 $u$ 在整个计算过程中一直待在栈底,直到最后才被弹出。整个计算过程就像一个大括号,中间的所有栈操作都在 $u$ 的“上方”进行,并且是自封闭的。
- PDA行为: $p \xrightarrow{a, \varepsilon \to u} r \xrightarrow{\text{中间字符串}} s \xrightarrow{b, u \to \varepsilon} q$。
- 中间的计算(从 $r$ 到 $s$)消耗了中间的字符串,并且栈的状态是从 u 的顶上开始,到 u 的顶上结束(净零变化)。这恰好对应了变量 $A_{rs}$ 的定义!
- 文法规则: 我们可以用规则 $A_{pq} \to a A_{rs} b$ 来模拟这种情况。
- 情况2: “分段”的计算: 第一个压入的符号 $u$ 在计算结束前的某个中间时刻就被弹出了。这意味着栈在计算的中间某个点(我们称之为状态 $r$)就变回空了。
- PDA行为: 整个计算可以被分解成两段独立的“从空栈到空栈”的计算。
- 第一段:从 $p$ (空栈) 走到 $r$ (空栈)。这对应了变量 $A_{pr}$。
- 第二段:从 $r$ (空栈) 走到 $q$ (空栈)。这对应了变量 $A_{rq}$。
- 文法规则: 我们可以用规则 $A_{pq} \to A_{pr} A_{rq}$ 来模拟这种情况。
📝 [总结]
本段提出了一个将PDA转换为CFG的绝妙思路。
- 预处理: 先将任意PDA标准化,使其只有一个接受状态、接受前清空栈、且每次转移只做压栈或弹栈一件事。
- 核心变量: 定义一种特殊的文法变量 $A_{pq}$,它能生成所有使PDA从状态 $p$ 到 $q$ 且栈净变化为零的字符串。
- 构造规则: 通过分析这种“净零变化”的计算路径的结构,发现它们只有两种基本模式:“配对”模式(一次压栈和一次弹栈包裹着一个子问题)和“分段”模式(一个问题分解为两个连续的子问题)。这两种模式恰好可以用CFG的两种标准形式的规则来描述:$A \to aBb$ 和 $A \to BC$。
🎯 [存在目的]
本段“证明思路”的目的是在展示形式化构造之前,揭示其背后深刻的洞察。这个洞察就是,任何复杂的PDA“净零栈变化”计算,都可以被递归地分解成上述两种简单的结构。而这两种结构又与CFG的产生式形式天然对应。这为看似不可能的任务(用文法描述机器)找到了一个突破口。
🧠 [直觉心智模型]
这就像在分析一段结构良好的代码(比如LISP代码)。
- 变量 $A_{pq}$: 代表一个能从状态 $p$ 正确解析到状态 $q$ 的函数 parse(p, q)。
- 情况1 (“配对”模式): 对应于括号匹配的结构 ( ... )。
- parse(p,q) 发现它要解析的字符串是以'('开头,以')'结尾。
- 它就调用 parse(r,s) 去解析括号里面的内容。
- 对应的规则就是 $A_{pq} \to ( A_{rs} )$。
- 情况2 (“分段”模式): 对应于顺序结构 ... ...。
- parse(p,q) 发现它要解析的字符串可以从中间某点 $r$ 分为两段。
- 它就先调用 parse(p,r) 解析第一段,然后调用 parse(r,q) 解析第二段。
- 对应的规则就是 $A_{pq} \to A_{pr} A_{rq}$。
任何复杂的代码块,都可以通过这两种方式递归地分解。这个思想与著名的CYK算法(用于解析CFG)有异曲同工之妙。
5.5. 证明
📜 [原文36]
证明 设 $P=\left(Q, \Sigma, \Gamma, \delta, q_{0},\left\{q_{\text {accept }}\right\}\right)$ 并构造 $G$。$G$ 的变量是 $\left\{A_{p q} \mid p, q \in Q\right\}$。开始变量是 $A_{q_{0}, q_{\text {accept}}}$。现在我们分三部分描述 $G$ 的规则。
- 对于每个 $p, q, r, s \in Q, u \in \Gamma$,以及 $a, b \in \Sigma_{\varepsilon}$,如果 $\delta(p, a, \varepsilon)$ 包含 ( $r, u$ ) 并且 $\delta(s, b, u)$ 包含 $(q, \varepsilon)$,则将规则 $A_{p q} \rightarrow a A_{r s} b$ 放入 $G$ 中。
- 对于每个 $p, q, r \in Q$,将规则 $A_{p q} \rightarrow A_{p r} A_{r q}$ 放入 $G$ 中。
- 最后,对于每个 $p \in Q$,将规则 $A_{p p} \rightarrow \varepsilon$ 放入 $G$ 中。
您可以通过以下数字获得此构造的一些见解。

图 2.28
与规则 $A_{p q} \rightarrow A_{p r} A_{r q}$ 对应的 PDA 计算

图 2.29
与规则 $A_{p q} \rightarrow a A_{r s} b$ 对应的 PDA 计算
📖 [逐步解释]
这一部分给出了引理2.27的正式构造性证明,将上一节的思路转化为三条具体的文法规则生成算法。
- 设置G的框架:
- 变量集: 对于PDA $P$ 的任意两个状态 $p, q \in Q$,我们都创建一个文法变量 $A_{pq}$。如果 $P$ 有 $N$ 个状态,那么 $G$ 将有 $N^2$ 个变量。
- 开始变量: $G$ 的开始变量被设定为 $A_{q_0, q_{\text{accept}}}$。这非常自然,因为我们的最终目标就是生成所有能让PDA从开始状态 $q_0$ 走到(唯一的)接受状态 $q_{\text{accept}}$,并且根据我们的预处理,此时栈也恰好为空。这正好是变量 $A_{q_0, q_{\text{accept}}}$ 的定义。
- 规则构造算法:
- 规则类型1 (配对规则): $A_{p q} \rightarrow a A_{r s} b$
- 生成条件: 当且仅当在PDA $P$ 中,存在一次压栈和一次弹栈形成了“配对”。
- $\delta(p, a, \varepsilon)$ 包含 $(r, u)$: 从状态 $p$ 读入 $a$,不弹栈,压入了符号 $u$,并进入状态 $r$。(这是配对的“开括号”)
- $\delta(s, b, u)$ 包含 $(q, \varepsilon)$: 从状态 $s$ 读入 $b$,弹出了同一个符号 $u$,并进入状态 $q$。(这是配对的“闭括号”)
- 生成规则: 对于这样一组 (p,q,r,s,u,a,b),我们就创建一条文法规则 $A_{pq} \to a A_{rs} b$。
- 直观解释 (图2.29): 这条规则完美地捕捉了图2.29所示的计算模式。整个从 $p$ 到 $q$ 的过程由三部分组成:消耗 $a$ 的第一步,消耗 $b$ 的最后一步,以及中间一段从 $r$ 到 $s$ 的、在 $u$ 上方进行的“净零栈变化”的子计算(由 $A_{rs}$ 生成)。
- 规则类型2 (分段规则): $A_{p q} \rightarrow A_{p r} A_{r q}$
- 生成条件: 对于任意三个状态 $p, q, r \in Q$。
- 生成规则: 我们为每一组 (p,q,r) 都创建一条规则 $A_{pq} \to A_{pr} A_{rq}$。
- 直观解释 (图2.28): 这条规则捕捉了图2.28所示的计算模式。一个从 $p$ 到 $q$ 的“净零栈变化”过程,可能在中间某个时刻,栈临时回到了初始高度(即变空),此时自动机处于状态 $r$。那么整个过程就可以被分解为两个独立的、更小的“净零栈变化”过程:一个从 $p$ 到 $r$,另一个从 $r$ 到 $q$。
- 规则类型3 (基本情况规则): $A_{p p} \rightarrow \varepsilon$
- 生成条件: 对于任意状态 $p \in Q$。
- 生成规则: 我们创建一条规则 $A_{pp} \to \varepsilon$。
- 直观解释: 这是递归的“出口”。一个PDA如何从状态 $p$ 走到状态 $p$ 并且栈净变化为零?最简单的方法就是什么都不做。这个过程消耗的输入字符串是 $\varepsilon$。所以,变量 $A_{pp}$ 必须能生成空字符串 $\varepsilon$。
📝 [总结]
本段给出了将(预处理过的)PDA转换为CFG的完整算法。
- 变量: 为每对状态 (p,q) 创建变量 $A_{pq}$。
- 开始变量: $A_{q_0, q_{\text{accept}}}$。
- 规则:
- 配对规则: 对每一个匹配的压栈-弹栈对,生成 $A_{pq} \to a A_{rs} b$。
- 分段规则: 对每一组状态 (p,q,r),生成 $A_{pq} \to A_{pr} A_{rq}$。
- 基本情况规则: 对每个状态 p,生成 $A_{pp} \to \varepsilon$。
🎯 [存在目的]
本段的目的是将上一节的抽象思路,转化为一个可以机械执行的、形式化的算法。通过这三条规则生成元语,我们可以将任何PDA的动态行为,完全编码进一个静态的CFG规则集合中。这为后续的正确性证明(即证明 $L(G) = L(P)$)提供了坚实的基础。
5.6. 断言 2.30
📜 [原文37]
现在我们通过证明 $A_{p q}$ 生成 $x$ 当且仅当 $x$ 可以将 $P$ 从 $p$(空栈)带到 $q$(空栈)来证明此构造有效。我们将“当且仅当”的每个方向视为一个单独的断言。
断言 2.30
如果 $A_{p q}$ 生成 $x$,那么 $x$ 可以将 $P$ 从 $p$(空栈)带到 $q$(空栈)。
我们通过对 $x$ 从 $A_{p q}$ 推导的步骤数进行归纳来证明此断言。
基础:推导有 1 步。
只有一步的推导必须使用右侧不包含变量的规则。在 $G$ 中,右侧不出现变量的唯一规则是 $A_{p p} \rightarrow \varepsilon$。显然,输入 $\varepsilon$ 将 $P$ 从 $p$(空栈)带到 $p$(空栈),因此基础得证。
归纳步骤:假设对于长度至多为 $k$ 的推导(其中 $k \geq 1$)成立,并证明对于长度为 $k+1$ 的推导成立。
假设 $A_{p q} \stackrel{*}{\Rightarrow} x$ 有 $k+1$ 步。此推导的第一步要么是 $A_{p q} \Rightarrow a A_{r s} b$,要么是 $A_{p q} \Rightarrow A_{p r} A_{r q}$。我们分别处理这两种情况。
在第一种情况下,考虑 $x$ 中由 $A_{r s}$ 生成的部分 $y$,因此 $x=a y b$。因为 $A_{r s} \xrightarrow{*} y$ 有 $k$ 步,归纳假设告诉我们 $P$ 可以从 $r$(空栈)到 $s$(空栈)。因为 $A_{p q} \rightarrow a A_{r s} b$ 是 $G$ 的一条规则,$\delta(p, a, \boldsymbol{\varepsilon})$ 包含 $(r, u)$ 并且 $\delta(s, b, u)$ 包含 $(q, \boldsymbol{\varepsilon})$,对于某些栈符号 $u$。因此,如果 $P$ 从 $p$(空栈)开始,读取 $a$ 后可以进入状态 $r$ 并将 $u$ 压栈。然后读取字符串 $y$ 可以将其带到 $s$ 并将 $u$ 保留在栈上。然后读取 $b$ 后可以进入状态 $q$ 并从栈中弹栈 $u$。因此,$x$ 可以将其从 $p$(空栈)带到 $q$(空栈)。
在第二种情况下,考虑 $x$ 中由 $A_{p r}$ 和 $A_{r q}$ 分别生成的部分 $y$ 和 $z$,因此 $x=y z$。因为 $A_{p r} \stackrel{*}{\Rightarrow} y$ 在至多 $k$ 步内,并且 $A_{r q} \stackrel{*}{\Rightarrow} z$ 在至多 $k$ 步内,归纳假设告诉我们 $y$ 可以将 $P$ 从 $p$ 带到 $r$,并且 $z$ 可以将 $P$ 从 $r$ 带到 $q$,并在开始和结束时清空栈。因此 $x$ 可以将其从 $p$(空栈)带到 $q$(空栈)。这完成了归纳步骤。
📖 [逐步解释]
这一部分开始证明构造的正确性,首先证明第一个方向:如果文法 $G$ 能生成 $x$,那么PDA $P$ 就能以“净零栈变化”的方式处理 $x$。证明方法是数学归纳法。
- 证明目标: $A_{pq} \Rightarrow^* x \implies x$ 能使 $P$ 从 $p$ (空栈) 到 $q$ (空栈)。
- 归纳法: 对推导的步数进行归纳。
- 归纳变量: $k$,即从 $A_{pq}$ 推导出 $x$ 所需的步骤数。
- 基础步骤 (Base Case): $k=1$
- 一步推导意味着使用的规则右侧不能有变量。
- 在我们构造的规则中,只有 $A_{pp} \to \varepsilon$ 满足这个条件。
- 所以,$A_{pp} \Rightarrow \varepsilon$。这里 $x=\varepsilon$, $p=q$。
- 我们需要证明:$\varepsilon$ 能使 $P$ 从 $p$ (空栈) 到 $p$ (空栈)。
- 这是显然的。PDA在状态 $p$,不读任何输入,计算0步,自然停在状态 $p$,栈也保持为空。基础成立。
- 归纳假设 (Inductive Hypothesis):
- 假设对于所有步数 $\le k$ 的推导,断言都成立。即,如果 $A_{uv} \Rightarrow^* y$ 的步数 $\le k$,那么 $y$ 就能使 $P$ 从 $u$ (空栈) 到 $v$ (空栈)。
- 归纳步骤 (Inductive Step): 证明对 $k+1$ 步成立
- 设 $A_{pq} \Rightarrow^* x$ 是一个 $k+1$ 步的推导。
- 考虑这个推导的第一步。根据我们的构造,只有两种可能:
- 情况1: $A_{pq} \Rightarrow a A_{rs} b$
- 那么 $x$ 的形式必然是 $ayb$,其中 $A_{rs} \Rightarrow^* y$。
- 这个子推导 $A_{rs} \Rightarrow^* y$ 的步数是 $k$,小于 $k+1$。
- 根据归纳假设,我们知道:字符串 $y$ 能使 $P$ 从 $r$ (空栈) 到 $s$ (空栈)。
- 同时,因为 $A_{pq} \to a A_{rs} b$ 这条规则存在,根据构造算法,我们知道PDA $P$ 中必然有转移 $\delta(p, a, \varepsilon)$ 包含 $(r, u)$ 和 $\delta(s, b, u)$ 包含 $(q, \varepsilon)$。
- 现在把它们串起来:
- $P$ 从 $p$ (空栈) 开始,读入 $a$,压入 $u$,到达状态 $r$。此时栈是 u。
- 从 $r$ 开始,处理字符串 $y$。因为 $y$ 能使 $P$ 从 $r$ (空栈) 到 $s$ (空栈),这意味着它也能使 $P$ 从 $r$ (栈为u) 到 $s$ (栈为u),因为它不会触碰到底部的 $u$。
- 现在 $P$ 在状态 $s$,栈是 u。它读入 $b$,根据转移,弹出 $u$,到达状态 $q$。此时栈为空。
- 综上,整个字符串 $x=ayb$ 使得 $P$ 从 $p$ (空栈) 走到了 $q$ (空栈)。情况1得证。
- 情况2: $A_{pq} \Rightarrow A_{pr} A_{rq}$
- 那么 $x$ 的形式必然是 $yz$,其中 $A_{pr} \Rightarrow^* y$ 并且 $A_{rq} \Rightarrow^* z$。
- 这两个子推导的步数都小于 $k+1$。
- 根据归纳假设:
- $y$ 能使 $P$ 从 $p$ (空栈) 到 $r$ (空栈)。
- $z$ 能使 $P$ 从 $r$ (空栈) 到 $q$ (空栈)。
- 把它们串起来:
- $P$ 从 $p$ (空栈) 开始,处理字符串 $y$,结束后到达状态 $r$,栈为空。
- 接着,从 $r$ (空栈) 开始,处理字符串 $z$,结束后到达状态 $q$,栈为空。
- 综上,整个字符串 $x=yz$ 使得 $P$ 从 $p$ (空栈) 走到了 $q$ (空栈)。情况2得证。
- 结论: 两种情况都证明完毕,归纳完成。断言2.30成立。
📝 [总结]
本段使用标准的数学归纳法,严格证明了PDA到CFG构造的“完备性”(Soundness):由构造出的文法 $G$ 生成的任何字符串,都确实对应着PDA $P$ 中的一个“净零栈变化”的计算路径。证明的关键在于,文法的两种递归规则(配对和分段)与PDA 计算路径的两种递归分解结构完美地一一对应。
5.7. 断言 2.31
📜 [原文38]
断言 2.31
如果 $x$ 可以将 $P$ 从 $p$(空栈)带到 $q$(空栈),那么 $A_{p q}$ 生成 $x$。
我们通过对 $P$ 从 $p$ 到 $q$ 的计算步骤数进行归纳来证明此断言,在输入 $x$ 上栈为空。
基础:计算有 0 步。
如果计算有 0 步,它在同一状态开始和结束——比如 $p$。所以我们必须证明 $A_{p p} \xrightarrow{*} x$。在 0 步内,$P$ 无法读取任何字符,所以 $x=\varepsilon$。根据构造,$G$ 有规则 $A_{p p} \rightarrow \varepsilon$,因此基础得证。
归纳步骤:假设对于长度至多为 $k$ 的计算(其中 $k \geq 0$)成立,并证明对于长度为 $k+1$ 的计算成立。
假设 $P$ 有一个计算,其中 $x$ 在 $k+1$ 步内将 $p$ 带到 $q$(空栈)。要么栈仅在此计算的开始和结束时为空,要么在其他地方也变空。
在第一种情况下,第一个动作中压栈的符号必须与最后一个动作中弹栈的符号相同。称此符号为 $u$。设 $a$ 为第一个动作中读取的输入,$b$ 为最后一个动作中读取的输入,$r$ 为第一个动作后的状态,$s$ 为最后一个动作前的状态。则 $\delta(p, a, \varepsilon)$ 包含 $(r, u)$ 并且 $\delta(s, b, u)$ 包含 $(q, \varepsilon)$,因此规则 $A_{p q} \rightarrow a A_{r s} b$ 在 $G$ 中。
设 $y$ 是 $x$ 中不包含 $a$ 和 $b$ 的部分,所以 $x=a y b$。输入 $y$ 可以将 $P$ 从 $r$ 带到 $s$,而不会触及栈上的符号 $u$,因此 $P$ 可以在输入 $y$ 上从 $r$(空栈)到 $s$(空栈)。我们已经移除了 $x$ 上的原始计算中 $k+1$ 步的第一步和最后一步,因此 $y$ 上的计算有 $(k+1)-2=k-1$ 步。因此,归纳假设告诉我们 $A_{r s} \xrightarrow{*} y$。因此 $A_{p q} \xrightarrow{*} x$。
在第二种情况下,设 $r$ 是栈变空的状态,而不是在对 $x$ 的计算的开始或结束时。则从 $p$ 到 $r$ 以及从 $r$ 到 $q$ 的计算部分各自包含至多 $k$ 步。设 $y$ 是第一部分计算期间读取的输入,$z$ 是第二部分计算期间读取的输入。归纳假设告诉我们 $A_{p r} \stackrel{*}{\Rightarrow} y$ 和 $A_{r q} \stackrel{*}{\Rightarrow} z$。因为规则 $A_{p q} \rightarrow A_{p r} A_{r q}$ 在 $G$ 中,$A_{p q} \stackrel{*}{\Rightarrow} x$,证明完成。
📖 [逐步解释]
这一部分证明了构造的另一个方向:如果PDA $P$ 能以“净零栈变化”处理 $x$,那么我们构造的文法 $G$ 一定能生成 $x$。这同样使用数学归纳法。
- 证明目标: $x$ 能使 $P$ 从 $p$ (空栈) 到 $q$ (空栈) $\implies A_{pq} \Rightarrow^* x$。
- 归纳法: 对PDA的计算步数进行归纳。
- 归纳变量: $k$,即 $P$ 处理 $x$ 所用的计算步骤数。
- 基础步骤 (Base Case): $k=0$
- 0步计算意味着 $P$ 没有移动。
- 因此,起点和终点是同一个状态 ($p=q$),并且没有消耗任何输入 ($x=\varepsilon$)。
- 我们需要证明 $A_{pp} \Rightarrow^* \varepsilon$。
- 根据构造算法的规则类型3,我们总是会生成 $A_{pp} \to \varepsilon$ 这条规则。
- 因此 $A_{pp} \Rightarrow \varepsilon$ 是一个一步推导。基础成立。
- 归纳假设 (Inductive Hypothesis):
- 假设对于所有步数 $\le k$ 的计算,断言都成立。
- 归纳步骤 (Inductive Step): 证明对 $k+1$ 步成立
- 设 $P$ 有一个 $k+1$ 步的计算,它处理 $x$ 并使 $P$ 从 $p$ (空栈) 到 $q$ (空栈)。
- 分析这个计算过程中栈的变化。因为开始和结束时栈都为空,且第一步必然是压栈,最后一步必然是弹栈,所以存在两种可能:
- 情况1: 中间栈不空: 栈只有在计算的开始和结束时才为空。
- 这意味着第一步压入的那个符号 $u$,直到最后一步才被弹出。
- 第一步: 必然是 $\delta(p, a, \varepsilon)$ 包含 $(r, u)$ 的形式(读 $a$,压** $u$,到 $r$)。
- 最后一步: 必然是 $\delta(s, b, u)$ 包含 $(q, \varepsilon)$ 的形式(读 $b$,弹** $u$,从 $s$ 到 $q$)。
- 根据构造算法的规则类型1,这两步转移的存在,保证了文法 $G$ 中一定有规则 $A_{pq} \to a A_{rs} b$。
- 中间的计算(从第2步到第k步)是在处理字符串 $x$ 的中间部分 $y$ ($x=ayb$)。这个子计算使得 $P$ 从 $r$ 走到 $s$,并且栈的变化是在 $u$ 的上方发生的,对 $u$ 没有影响。这等价于一个在空栈上从 $r$ 到 $s$ 的计算。
- 这个子计算的步数是 $(k+1)-2 = k-1$,小于 $k+1$。
- 根据归纳假设,我们知道 $A_{rs} \Rightarrow^* y$。
- 因此,我们可以构造一个推导: $A_{pq} \Rightarrow a A_{rs} b \Rightarrow^* a y b = x$。情况1得证。
- 情况2: 中间栈变空: 栈在计算过程中的某个中间时刻 $t_i$ ($0 < i < k+1$) 也变为空。
- 让 $r$ 是在第一个这样的中间时刻,PDA所处的状态。
- 那么整个计算可以被自然地分成两段:
- 第一段: 从 $p$ (空栈) 到 $r$ (空栈)。设其消耗的输入为 $y$。这段计算的步数 $\le k$。
- 第二段: 从 $r$ (空栈) 到 $q$ (空栈)。设其消耗的输入为 $z$。这段计算的步数也 $\le k$。
- 根据归纳假设,我们知道:
- $A_{pr} \Rightarrow^* y$
- $A_{rq} \Rightarrow^* z$
- 根据构造算法的规则类型2,我们知道文法 $G$ 中一定有规则 $A_{pq} \to A_{pr} A_{rq}$。
- 因此,我们可以构造一个推导: $A_{pq} \Rightarrow A_{pr} A_{rq} \Rightarrow^* y A_{rq} \Rightarrow^* yz = x$。情况2得证。
- 结论: 两种情况都证明完毕,归纳完成。断言2.31成立。
📝 [总结]
本段使用数学归纳法,严格证明了PDA到CFG构造的“正确性”(Completeness):任何一个PDA $P$ 的“净零栈变化”的计算路径,都确实能被我们构造的文法 $G$ 所生成。证明的关键在于,PDA的计算路径结构(无论是“配对”型还是“分段”型)都能被文法的相应规则所捕捉。
5.8. 结论
📜 [原文39]
这完成了引理 2.27 和定理 2.20 的证明。
我们刚刚证明了下推自动机识别上下文无关语言的类。这个证明使我们能够建立正则语言和上下文无关语言之间的关系。因为每个正则语言都由一个有限自动机识别,并且每个有限自动机都是一个简单地忽略其栈的下推自动机,所以我们现在知道每个正则语言也是一个上下文无关语言。
推论 2.32
每个正则语言都是上下文无关语言。

图 2.33
正则语言和上下文无关语言的关系
📖 [逐步解释]
这一部分是本节的收尾,总结了证明的完成,并得出了一个重要的推论。
- “这完成了引理 2.27 和定理 2.20 的证明”:
- 断言2.30和2.31共同证明了我们的构造是正确的,即对于任意 $p,q$,变量 $A_{pq}$ 生成的语言,恰好就是使 $P$ 从 $p$ (空栈) 到 $q$ (空栈) 的字符串集合。
- 这也就证明了引理2.27(因为开始变量 $A_{q_0, q_{\text{accept}}}$ 生成的语言就是 $P$ 接受的语言)。
- 引理2.21 (CFG->PDA) 和 引理2.27 (PDA->CFG) 合在一起,就完整地证明了定理2.20(PDA与CFG等价)。
- “我们刚刚证明了下推自动机识别上下文无关语言的类”:
- “这个证明使我们能够建立正则语言和上下文无关语言之间的关系”:
- 定理2.20是一个强大的工具,我们可以用它来推导其他结论。这里就是要用它来明确正则语言和上下文无关语言的层级关系。
- “因为每个正则语言都由一个有限自动机识别...”:
- “...并且每个有限自动机都是一个简单地忽略其栈的下推自动机...”:
- 这是一个关键的洞察。一个有限自动机(FA)可以被看作是一个特殊的PDA。
- 这个特殊的PDA只是从不使用它的栈。它的所有转移规则都形如 a, ε -> ε,即不看不弹也不压。
- 或者说,FA的转移函数 $\delta_{FA}(q, a)$ 可以被直接翻译成PDA的转移函数 `$\delta_{PDA}(q, a, \varepsilon) = \{(p, \varepsilon)\}`` (如果 $\delta_{FA}(q, a)=p$)。
- 既然任何FA都可以被视为一个(非常简单的)PDA,那么FA能识别的语言,PDA当然也能识别。
- “...所以我们现在知道每个正则语言也是一个上下文无关语言”:
- 语言 L 是正则的。
- => 存在一个FA $M$ 识别 L。
- => $M$ 可以被看作一个PDA $M'$。
- => 语言 L 可以被一个PDA $M'$ 识别。
- => 根据定理2.20,语言 L 是上下文无关的。
- “推论 2.32: 每个正则语言都是上下文无关语言”:
- 将上述结论提炼为一个推论(Corollary)。
- 推论是在一个定理被证明之后,能够被轻松、直接地得出的结论。
- 图 2.33:
- 用一个韦恩图(Venn Diagram)直观地展示了这个关系。
- 上下文无关语言的集合是一个大圈。
- 正则语言的集合是完全包含在大圈内部的一个小圈。
- 这表明,正则语言是上下文无关语言的一个真子集(proper subset)。也就是说,所有正则语言都是上下文无关的,但存在一些上下文无关语言(如 $\{0^n1^n\}$)不是正则的。
📝 [总结]
本节在完成了PDA与CFG等价性(定理2.20)的证明后,立即应用这个强大的定理,得出了一个重要的推论(推论2.32):正则语言类是上下文无关语言类的一个子集。其论证逻辑是,任何有限自动机都可以被视为一个不使用其栈的下推自动机,因此FA识别的语言也一定能被PDA识别,故根据定理2.20,这些语言都是上下文无关的。图2.33形象地展示了这种包含关系。
6行间公式索引
- 下推自动机$M_1$的形式化定义组件,用于识别语言 $\{0^n1^n \mid n \ge 0\}$
$$
\begin{aligned}
& Q=\left\{q_{1}, q_{2}, q_{3}, q_{4}\right\}, \\
& \Sigma=\{0,1\}, \\
& \Gamma=\{0, \$\}, \\
& F=\left\{q_{1}, q_{4}\right\}, \text { 并且 }
\end{aligned}
$$
- 一个需要非确定性来识别的语言,其中a的数量等于b或c的数量
$$
\left\{\mathrm{a}^{i} \mathrm{~b}^{j} \mathrm{c}^{k} \mid i, j, k \geq 0 \text { 且 } i=j \text { 或 } i=k\right\} 。
$$
- 偶数长度的回文语言,其中w是任意01串,w^R是其反转
$$
\left\{w w^{\mathcal{R}} \mid w \in\{0,1\}^{*}\right\}
$$
- 将整个字符串一次性压栈的简写操作的实现方式
$$
\begin{aligned}
& \delta(q, a, s) \text { 包含 }\left(q_{1}, u_{l}\right), \\
& \delta\left(q_{1}, \varepsilon, \varepsilon\right)=\left\{\left(q_{2}, u_{l-1}\right)\right\}, \\
& \delta\left(q_{2}, \varepsilon, \varepsilon\right)=\left\{\left(q_{3}, u_{l-2}\right)\right\}, \\
& \vdots \\
& \delta\left(q_{l-1}, \varepsilon, \varepsilon\right)=\left\{\left(r, u_{1}\right)\right\} .
\end{aligned}
$$
- 一个具体上下文无关文法G,用于后续转换为PDA的示例
$$
\begin{aligned}
& S \rightarrow \mathrm{a} T \mathrm{~b} \mid \mathrm{b} \\
& T \rightarrow T \mathrm{a} \mid \varepsilon
\end{aligned}
$$
[[END_OF_RESPONSE]]所有解释内容已全部输出完毕。