你有没有在玩具店里见过一套庞大的乐高机械组?盒子里的零件拆开来,是几百颗基础砖块:红色 2×4 板、蓝色长轴、白色面板。真正让这些砖块变成一辆跑车的,是一本薄薄的《拼装手册》——它告诉你“车轮组件”由哪些砖块拼成,“车身组件”又由哪几个组件组合。你照着手册层层拆解,从“最终产品”一路拆到每一颗砖块,最终得到一棵漂亮的拼装结构树。 在计算机科学里,描述“怎样从基础符号合法地组合成复杂句子”的这本手册,就叫上下文无关文法(Context-Free Grammar,CFG)。它规定什么算“合法句子”,编译器靠它判断代码对不对,聊天机器人靠它理解命令的骨架,数据处理工具也靠它校验配置文件的嵌套结构。本文就带你用乐高拼装车间这条线索,把 CFG 从四元组定义、推导与归约、二义性判定,到乔姆斯基范式与 GNF 转换,再到 Yacc 解析器生成器,完整走一遍。

岗位关联

学会 CFG,你就掌握了编译器与 NLP 工具背后的语言骨架。当你要用 LLM 构建结构化输出、设计领域 DSL 或解析器时,不会再被“语法分析”这个黑盒卡住——这是 AI 从业者最硬核的底层素养之一。

目录
  1. 可视化演示
  2. 文法核心概念
  3. CFG四元组定义
  4. 推导与归约
  5. 二义性判定
  6. 乔姆斯基范式
  7. Greibach范式
  8. 乔姆斯基分类
  9. 解析器应用
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

上下文无关文法是语法分析的理论基础,通过产生式规则定义语言结构。本文详解CFG的四元组形式化定义、最左推导与最... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

CFG四元组:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

文法核心概念

想象你刚被乐高工厂录用为拼装员,工位前是一堆亮闪闪的基础砖块:红色 2×4 板、蓝色长轴、白色面板。这些砖块在车间里被称为“终端件”,因为任何工序都无法再拆分它们——在编译原理里,它们就叫终结符(Terminal)。而墙上贴着的“车身组件”“引擎组件”则不是实物,它们是工人们拼装过程中的半成品状态,最终都要被拆解成砖块——这就是非终结符(Nonterminal)。

把目光从车间拉回编译器。编译器要判断一段源代码 a + b * c 是不是“合法句子”,就需要一本“拼装手册”,也就是文法(Grammar)。文法由四条核心概念组成:终结符是句子里的最小原子(如 id、+、*、括号);非终结符是“表达式”“项”这类待展开的半成品;开始符号(Start Symbol)是整本手册指定的唯一初始目标;而产生式(Production)就是手册上的一条条指令,比如“表达式可以拼成 表达式 加 表达式”。

形式化地说,一个上下文无关文法用一套产生式规则描述语言。看一个最简单的算术表达式文法:

在乐高车间里,这张文法就是《拼装手册》第 1 页的指令:一个“车身组件”可以是一块底盘加一个驾驶室,也可以是一块底盘加一个车斗,还可以直接就是一个现成的车壳。工人看着手册,按订单选择最合适的那条指令。

G:E→E+E∣E×E∣(E)∣idG:\quad E \rightarrow E + E \mid E \times E \mid (E) \mid id
这个文法的意思是:一个“表达式 E”可以由“E 加 E”“E 乘 E”“括号包住 E”或“任意标识符 id”四种方式拼出来。| 表示“或”。
  1. E 作为唯一的非终结符它代表“一个表达式”这个半成品状态,需要被继续展开成具体砖块。
    EE
  2. 产生式右部的四种候选每条 E → ... 都提供了一种把 E 展开的合法方式。
    E+E,E×E,(E),idE + E, \quad E \times E, \quad (E), \quad id
  3. 竖线表示“或”候选四种右部任选其一,就像手册同一页写着四种拼法。
    ∣\mid
  4. id 是终结符它代表任意变量名或数字字面量,是整个推导树的叶子节点。
    idid

CFG四元组定义

乐高工厂为了规范化管理,会把《拼装手册》拆成四份台账:一份登记所有半成品夹具名(V),一份登记所有基础砖块类型(T),一份登记所有拼装指令(P),还有一份在封面标明最终产品名(S)。任何一本合法的拼装手册,都必须能装进这四个抽屉里——CFG 的形式化定义正是这“四个抽屉”。

这个定义叫做四元组:

正是这四个抽屉,把“拼装手册”变成机器可检索的数学对象。无论多复杂的语言规则,只要你能填满这四个抽屉,就得到了一个上下文无关文法。

G=(V,T,P,S),其中{V={E},T={id,+,×,(,)},P={E→E+E,  E→E×E,  E→(E),  E→id},S=EG = (V, T, P, S), \quad \text{其中} \quad\begin{cases}V = \{E\}, \\T = \{id, +, \times, (, )\}, \\P = \{E \to E+E, \; E \to E\times E, \; E \to (E), \; E \to id\}, \\S = E\end{cases}
四元组是 CFG 的“身份证”。V 是非终结符集合,T 是终结符集合,P 是产生式集合,S 是开始符号。P 中每条规则的左边必须恰好是一个非终结符——这正是“上下文无关”的含义:替换一个符号时,不关心它出现在什么上下文里。
  1. V 变量集合V 是所有“半成品夹具”的名单,这里的集合里只有 E 一个非终结符。
    V={E}V = \{E\}
  2. T 终结符集合T 是所有“最小砖块”的名单,它们是句子的字母表,与 V 互不相交。
    T={id,+,×,(,)}T = \{id, +, \times, (, )\}
  3. P 产生式集合P 是拼装指令清单,每条指令左部只有一个非终结符,右部是终结符与非终结符组成的串。
    P={E→E+E,  E→E×E,  E→(E),  E→id}P = \{E \to E+E, \; E \to E\times E, \; E \to (E), \; E \to id\}
  4. S 开始符号S 是“最终产品”的名字,所有拼装都从它出发,且 S 必须属于 V。
    S=ES = E

推导与归约

拼装员拿到一张订单——目标串 id + id。他翻开手册,从封面上的“最终产品”E 出发,一路按指令把半成品拆成组件:E 先变成 E + T,再把左边的 E 变成 T,然后 T 变成 id……这种逐步展开的过程就叫推导(Derivation)。

推导就是反复用产生式的右部替换串中的某个非终结符。选择哪个非终结符替换,会带来不同的策略:最左推导(Leftmost Derivation)每步总替换当前串中最左边的非终结符;最右推导(Rightmost Derivation)每步替换最右边的非终结符;而归约(Reduction)是推导的逆过程——把一串符号“合并”回一个非终结符,这正是各类移进-归约分析器的核心动作。

考察文法 E → E + T | T、T → id,对串 id + id 做最左推导:

这就像流水线上永远先处理“最左边”的半成品工位。与之相对的最右推导则永远先处理最右边;对于无二义文法,两种策略最终会得到同一棵语法分析树——这个性质在后面讲 Yacc 时会成为“如何构造唯一语法树”的基石。

E⇒E+T⇒T+T⇒id+T⇒id+idE \Rightarrow E + T \Rightarrow T + T \Rightarrow id + T \Rightarrow id + id
每一步 ⇒ 表示应用一条产生式替换当前串的最左非终结符。最终得到只含终结符的串,推导结束。把箭头反过来看,就是从 id + id 归约回 E。
  1. 第一次替换把最左的 E 用产生式 E → E + T 展开成两项之和。
    E⇒E+TE \Rightarrow E + T
  2. 第二次替换现在串是 E + T,最左非终结符还是 E,用 E → T 替换它。
    E+T⇒T+TE + T \Rightarrow T + T
  3. 第三次替换串变成 T + T,最左的 T 用 T → id 替换成终结符。
    T+T⇒id+TT + T \Rightarrow id + T
  4. 第四次替换剩下的 T 也替换成 id,得到纯终结符串,推导完成。
    id+T⇒id+idid + T \Rightarrow id + id

二义性判定

工厂收到一份特殊订单 id + id + id,两组工人各按手册拼装。A 组组长说“按左结合,从左往右加”;B 组组长说“按右结合,从右往左加”。两组都产出了合法产品,但内部装配关系完全不同——车间主任震怒了:同一张图纸怎么能有两种拼法?这就叫二义性(Ambiguity)。

一个 CFG 是二义的,如果存在某个由它生成的串,对应两棵不同的语法分析树。注意二义性是“文法”的性质,而不是“语言”的必然属性:同一个语言可以有一个二义文法,也可能存在无二义文法。考察文法 E → E + E | id 和串 id + id + id,可以构造两个不同的最左推导:

判定一个文法是否二义,理论上不可判定,但实践里只要找出一个具体的“双拼案例”就能敲定。在 Yacc 中,二义性会表现为 shift/reduce 冲突,工程师通常通过优先级声明或改写文法来消除它——就像组长开会统一“默认左结合”的规矩。

E⇒E+E⇒E+E+E⇒id+E+E⇒id+id+E⇒id+id+idE \Rightarrow E+E \Rightarrow E+E+E \Rightarrow id+E+E \Rightarrow id+id+E \Rightarrow id+id+id
E⇒E+E⇒id+E⇒id+E+E⇒id+id+E⇒id+id+idE \Rightarrow E+E \Rightarrow id+E \Rightarrow id+E+E \Rightarrow id+id+E \Rightarrow id+id+id
两条推导都合法,但第一步展开的位置不同,导致语法树的形状不同:第一条对应“先左后右”的左结合结构,第二条对应不同的嵌套方式。同一个串、同一本文法,两棵不同的树——二义性实锤。
  1. 推导一的第二步在串 E + E 中继续展开最左的非终结符 E,形成 E + E + E,对应“左结合”。
    E+E⇒E+E+EE + E \Rightarrow E + E + E
  2. 推导二的第二步同样在串 E + E 中,选择把最左的 E 替换成终结符 id,而不是继续展开。
    E+E⇒id+EE + E \Rightarrow id + E
  3. 两条推导殊途同归两个不同的最左推导最终都得到同一个终结符串 id + id + id,但语法树结构不同。
    id+id+idid + id + id

乔姆斯基范式

为了全工厂统一质量,品控部推出“标准件制度”:每一条拼装指令最多只能涉及“两个半成品”,或者直接使用“一个基础砖块”。任何复杂的手册都可以改造成这种标准格式,而产出的产品集合一字不差。这就是乔姆斯基范式(Chomsky Normal Form,CNF)。

CNF 规定每条产生式只能有两种形状:A → BC(右部恰为两个非终结符)或 A → a(右部恰为一个终结符)。另外允许 S → ε,但仅当空串属于该语言时才允许。把任意 CFG 转成 CNF 通常分三步:先消除 ε 产生式,再消除单元产生式(A → B 这种“直接改名”),最后把长度大于 2 的候选拆成二元链。

CNF 的意义不只是品控洁癖——CYK 动态规划算法就建立在这种“二元拆分”之上:一段字符串要归约成 A,就必须能拆成两段,分别归约成 B 和 C。这就像检验复杂拼装是否合规,只需递归检查每一个二元节点是否合规。

A→BC或A→a,允许  S→εA \rightarrow B C \quad \text{或} \quad A \rightarrow a, \quad \text{允许} \; S \rightarrow \varepsilon
CNF 是一切“标准件”的代名词:产生式要么是“两个非终结符”,要么是“一个终结符”,只有开始符号有权产生空串。转换前后语言完全一致,只是指令粒度变细了。
  1. 二元产生式 A → BC右部恰为两个非终结符,这是“标准件”的第一种型号。
    A→BCA \rightarrow BC
  2. 终结符产生式 A → a右部恰为一个终结符,这是“标准件”的第二种型号。
    A→aA \rightarrow a
  3. S → ε 特例只有当空串属于该语言时,才允许 S 产生空串,并且此时 S 不得出现在任何产生式的右部。
    S→εS \rightarrow \varepsilon
  4. 等价性保真变换前后文法生成的语言完全一致,只是“拼装指令”被细化成标准动作。
    L(G′)=L(G)L(G') = L(G)

Greibach范式

如果说 CNF 是“标准件制度”,那么 Greibach 范式(Greibach Normal Form,GNF) 是更严格的新规:每条拼装指令的“第一步动作必须是拿起一个基础砖块”,后续才是继续拼半成品。也就是说,任何合法产品的拼装过程,第一个被放上去的永远是一片砖,而不是一个半成品。

GNF 规定每条产生式形如 A → aβ,其中 a 是终结符,β 是若干个非终结符组成的串(可以为空)。转换的核心难点是消除左递归:像 A → Aα 这样的规则会让最左推导永远卡在 A 上停不下来;标准做法是引入新非终结符 Z,把 A → Aα | β 改写成 A → βZ、Z → αZ | ε。

GNF 在理论上很优雅:它让“最左推导的每一步都消耗一个终结符”,从而与“下推自动机读入字母”形成一一对应。直观地说,GNF 相当于把每个半成品的拼装过程都规范成“第一块必然是砖头”,这让从文法到自动机的构造畅通无阻。

A→a X1X2⋯Xn(n≥0)A \rightarrow a \, X_1 X_2 \cdots X_n \quad (n \ge 0)
GNF 的招牌特征是“顶部终结符”:右部第一位必须是终结符 a,后面跟零到多个非终结符。这使得最左推导的每一步都读入一个终结符,与自动机读入字符的节奏完美对齐。
  1. 顶部的终结符 a右部第一位必须是终结符,这是 GNF 最醒目的标志。
    aa
  2. 后续非终结符串每个 X_i 都是非终结符,它们会在后续推导中被继续展开。
    X1X2⋯XnX_1 X_2 \cdots X_n
  3. n = 0 的退化情况产生式退化为 A → a,与 CNF 的终结符产生式一致。
    A→aA \rightarrow a
  4. 左递归消除技巧把 A → Aα | β 改写成引入新符号 Z 的右递归形式,是转换 GNF 的关键一步。
    A→βZ,Z→αZ∣εA \rightarrow \beta Z, \quad Z \rightarrow \alpha Z \mid \varepsilon

乔姆斯基分类

乐高总部发现世界上的拼装手册种类繁多,于是把手册按指令力度分成四个等级:最弱的手册只允许“把一个零件整体替换成另一个零件”(正则文法);稍强的手册允许“按上下文情境替换”(上下文有关文法);我们这本车间手册恰好属于第二强等级(上下文无关文法);而最强的 0 型手册几乎不加限制,能描述图灵机的全部运算过程。这个分级就是乔姆斯基谱系(Chomsky Hierarchy)。

谱系把形式文法分为四型:0 型无限制文法(对应图灵机),1 型上下文有关文法(产生式形如 αAβ → αγβ,替换时要求 A 身边有 α 和 β),2 型上下文无关文法(即 CFG,左部只有一个非终结符),3 型正则文法(产生式形如 A → aB | a,对应有限自动机)。四种语言集合严格嵌套:

理解了这条谱系,你就明白为什么编译器前端选择 CFG 而非正则表达式——正则文法“记不住嵌套”,而 CFG 拥有“括号嵌套、表达式中套子表达式”的递归生成能力。这正是拼装手册被选为车间主手册的根本原因。

L3⊂L2⊂L1⊂L0\mathcal{L}_3 \subset \mathcal{L}_2 \subset \mathcal{L}_1 \subset \mathcal{L}_0
集合包含关系说明:每一个正则语言都是上下文无关语言,每一个上下文无关语言都是上下文有关语言,依此类推。反过来则不然——比如 aⁿbⁿ 是上下文无关语言,却不是正则语言。
  1. 3 型语言是 2 型的真子集每个正则语言都能用 CFG 生成,但存在 CFG 能生成而正则文法不能生成的语言。
    L3⊂L2\mathcal{L}_3 \subset \mathcal{L}_2
  2. 2 型语言是 1 型的真子集所有上下文无关语言都能用上下文有关文法描述。
    L2⊂L1\mathcal{L}_2 \subset \mathcal{L}_1
  3. 1 型语言是 0 型的真子集上下文有关语言只是无限制语言世界中的一小片。
    L1⊂L0\mathcal{L}_1 \subset \mathcal{L}_0

解析器应用

光有手册还不够,工厂里还需要一台“自动拼装机器人”。给机器人输入文法,它就能自动识别订单里的目标串并检查是否合规——这台机器就是解析器生成器(Parser Generator),最著名的当属 Yacc/Bison。工程师们写的不是分析算法,而是“手册的电子版”。

在 Yacc 的规则文件里,一条语法规则长这样:

其中 expression 是一个非终结符,冒号等价于产生式箭头 →,每个 | 分隔一种候选右部,分号收尾。Yacc 会根据这些产生式自动生成一个 LALR 分析器:它从左到右扫描输入串,把终结符“移进”栈里,当栈顶形成某个产生式右部时,将其“归约”成左部非终结符——这正是前面讲过的“归约”概念在工程中的直接体现。

当你在项目里用 Yacc/Bison 定义语法时,其实就是在填那四个抽屉:终结符是 token 声明,非终结符写在规则左侧,开始符号写在 %start 里,而 P 就是这一整段规则清单。从“乐高手册”到“自动拼装机”,CFG 理论就这样完整地落到了工程里。

expr→expr+expr∣expr−expr∣numberexpr \rightarrow expr + expr \mid expr - expr \mid number
在 Yacc 中,冒号就是产生式箭头,| 表示“或”,分号结束一条规则。小写名称(如 expr)是非终结符,词法层返回的 token(如 number)是终结符。
  1. expr 作为左部它是非终结符,对应 Yacc 规则里写在冒号左侧的名字。
    exprexpr
  2. 右部的两种运算候选加法和减法各对应一种展开方式,都是“表达式 加/减 表达式”。
    expr+expr,expr−exprexpr + expr, \quad expr - expr
  3. 竖线连接多个候选Yacc 的 | 与数学上的“或”同义,分隔同一条规则的多种右部。
    ∣\mid
  4. number 终结符由词法分析器产生的 token 是终结符,在右部直接出现,不再被展开。
    numbernumber
yacc4 行
expression : expression '+' expression           | expression '-' expression           | number           ;

AI 应用场景

编译器与 AI 编译器的前端语法分析

阶段:语法分析(编译器前端第二环)

解决的问题:手写递归下降解析器要维护大量回溯逻辑,极易出错;用 Yacc/ANTLR 这类解析器生成器,只需把 CFG 产生式声明清楚,工具就会自动生成 LALR 分析器,把“怎么解析”交给工具,把“解析什么”交给文法。AI 编译器(如 TVM、MLIR)的 IR 语法同样用类 CFG 描述,工程师每天都在与文法打交道。

具体案例:某团队为内部 DSL 设计语法,写下 30 条产生式,Yacc 自动生成 1500 行 C 代码的解析器,解析速度提升 3 倍,逻辑全部集中在文法声明里,维护成本大幅下降。

大模型输出中的结构化命令校验

阶段:AI Agent 的意图解析与输出约束

解决的问题:LLM 生成的文本自由度太高,直接交给下游系统可能触发非法操作。用 CFG 定义一组“合法指令模板”,例如“播放 [歌曲名]”“查询 [城市] [天气]”,模型输出先经过 CFG 校验,非法结构直接拦截,比纯正则表达式可靠得多。

具体案例:某智能客服系统用 ANTLR 为指令集定义 CFG,LLM 生成的用户意图被解析成结构化调用参数,槽位抽取准确率从 78% 提升到 95%,且天然防注入攻击。

Python 演示

python47 行
# -*- coding: utf-8 -*-def cyk_parse(grammar, start, string):    # CYK 算法: 判断 string 能否由 CNF 文法生成    n = len(string)    if n == 0:        return False  # 本示例文法不产生空串, 简化处理    # table[i][j] 是能推导出 string[i:j+1] 的非终结符集合    table = [[set() for _ in range(n)] for _ in range(n)]     # 第一步: 处理 A -> a 形式的终结符产生式    for i, ch in enumerate(string):        for A, prods in grammar.items():            for rhs in prods:                if len(rhs) == 1 and rhs[0] == ch:                    table[i][i].add(A)     # 第二步: 按子串长度从小到大填表    for length in range(2, n + 1):        for i in range(n - length + 1):            j = i + length - 1            for k in range(i, j):                for A, prods in grammar.items():                    for rhs in prods:                        if len(rhs) == 2 and rhs[0] in table[i][k] and rhs[1] in table[k + 1][j]:                            table[i][j].add(A)     return start in table[0][n - 1]  def main():    # 教材经典文法 G:    # S -> AB | BC    # A -> BA | a    # B -> CC | b    # C -> AB | a    grammar = {        'S': ['AB', 'BC'],        'A': ['BA', 'a'],        'B': ['CC', 'b'],        'C': ['AB', 'a']    }    for w in ['baaba', 'baa', 'abab']:        print("{} 属于 L(G): {}".format(w, cyk_parse(grammar, 'S', w)))  if __name__ == '__main__':    main()
目的:实现 CYK 动态规划算法,用 CNF 文法判断一个字符串是否属于该上下文无关语言,是语法分析的理论原型。
  1. table = [[set() for _ in range(n)] for _ in range(n)]建立 n×n 三角表,规则是“能推出 string[i:j+1] 的所有非终结符”。
  2. if len(rhs) == 1 and rhs[0] == ch:先处理 A → a 这类终结符产生式,一个字符能被谁直接推出。
  3. for k in range(i, j):枚举切分点 k,把子串 [i..j] 分成两段,这是 CYK 的“二元拆分”核心。
  4. if len(rhs) == 2 and rhs[0] in table[i][k]只有当左半能归约成 B、右半能归约成 C,且存在 A → BC 时,整段才能归约成 A。
  5. return start in table[0][n - 1]整串若能归约成开始符号 S,则该串属于文法语言。

常见误区

误区“上下文无关文法不就是随便写规则吗?规则两边不用管周围是什么,这名字取得太随意了。”

正解“上下文无关”的准确含义是:产生式左侧只能有一个非终结符,即 A → α 这种替换不依赖 A 出现在什么样的上下文中。对比 1 型上下文有关文法 αAβ → αγβ,替换 A 时要求周围必须有 α 和 β。类比乐高车间:CFG 的规则就像“只要看到'车身组件'这个夹具名,不管它在哪条流水线上,都按同一条指令展开”;而上下文有关规则则会说“只有在驾驶室后面的车身组件才这样拼”。

为何常见:名字太过口语化,初学者把它理解成否定句,忽略了它真正强调的是产生式左部形式的限制。

误区“这个文法有二义性,所以它不能用于编译器,整个语言设计都得换掉。”

正解二义性说的是同一个文法存在某个串对应多棵不同的语法树,这是文法的属性,不是语言的属性。实际工程里,很多语言(比如 C 语言的 if-else 悬空问题)用二义文法描述更自然;Yacc 会报 shift/reduce 冲突,工程师通过优先级声明或改写文法来消除冲突,而不是换语言。类比:同一本手册有两种拼法,车间主任不需要换产品,只需在手册上加一条备注“默认左结合”。

为何常见:初学者把“二义性”当成文法的死罪,其实一个二义文法往往存在等价的、无二义的其他文法,语言本身仍可被优雅描述。

误区“转成乔姆斯基范式后,产生式被拆得七零八落,能识别的字符串肯定也变少了吧?”

正解CNF 转换是等价变换,不改变生成的语言。只是产生式的粒度变细了:长产生式被拆成多步,终结符也被包了一层非终结符。类比:把“熟练工一步装好发动机”改成“流水线分三步装”,最终产品一模一样,只是工序标准化了。CYK 算法恰恰建立在 CNF 上,因为“二元拆分”正好匹配字符串的两段划分——这是把文法转成 CNF 的最大价值。

为何常见:看到语法树变深、中间符号变多,容易误以为“语言变小了”;实际上原有句子的所有生成路径都仍存在,只是多拐了几道弯。

误区“最左推导和最右推导得到的语法树不一样吧?所以选用不同的推导顺序会生成不同的句子。”

正解对于无二义文法,任何推导顺序(最左、最右或其它顺序)都对应同一棵语法分析树,只是节点展开的先后次序不同。对于二义文法,不同推导确实得到不同树,但那是二义性本身的定义,而不是推导策略造成的。类比:无论先拼左轮还是先拼右轮,只要按同一张图纸,最终车的装配结构是一样的。

为何常见:把“推导过程的先后顺序”和“树的结构”混为一谈——顺序是时间维度的操作,结构是空间维度的结果。

面试题

Q1. 一个上下文无关文法 G = (V, T, P, S) 中,S 代表什么?

单选题 · easy

  • A.终结符集合
  • B.产生式集合
  • C.开始符号← 正确答案
  • D.变量集合

答案:C。S 是 Start Symbol 的缩写,它是推导的起点,属于 V 且必须是非终结符。在乐高比喻里,S 就是手册封面上的“最终产品名”。A 选项是 T,B 选项是 P,D 选项是 V。

Q2. 以下哪一组是 CNF 产生式的两种合法形状?

单选题 · easy

  • A.A → BC 与 A → a← 正确答案
  • B.A → aB 与 A → Ba
  • C.A → ABCD 与 A → ε
  • D.AB → CD 与 A → B

答案:A。CNF 只允许右部为“两个非终结符”或“一个终结符”。B 是右线性(3 型)文法;C 的长产生式必须先拆成二元链,且除 S 外不允许 ε;D 的 AB → CD 左侧有两个符号,破坏了“上下文无关”的单符号左部要求。

Q3. 文法 E → E + E | id 关于串 id + id + id 是( )。

单选题 · easy

  • A.正规文法
  • B.无二义的
  • C.二义的← 正确答案
  • D.无法生成的

答案:C。该串存在两个不同的最左推导,对应两棵不同的语法树(左结合与右结合),所以该文法是二义的。类比:同一本手册,两组工人拼出了内部装配关系不同的产品。

Q4. 在乔姆斯基谱系中,上下文无关文法是第几型文法?

单选题 · easy

  • A.0 型
  • B.1 型
  • C.2 型← 正确答案
  • D.3 型

答案:C。0 型对应图灵机,1 型对应上下文有关,2 型对应上下文无关,3 型对应正则。CFG 属于 2 型,覆盖了绝大多数编程语言的核心语法结构。

Q5. 最左推导每一步替换的是( )。

单选题 · mid

  • A.最左边的终结符
  • B.最右边的非终结符
  • C.最左边的非终结符← 正确答案
  • D.任意一个非终结符

答案:C。最左推导要求每一步都替换当前串中最左边的非终结符,直到串中不再有非终结符。类比流水线:永远先处理最左边的半成品工位。A 错在终结符不可替换;B 是最右推导;D 是脱离定义的任意策略。

Q6. 在 Yacc 规则文件 expression : expression '+' expression ; 中,冒号等价于( )。

单选题 · mid

  • A.赋值运算符
  • B.产生式箭头 →← 正确答案
  • C.相等比较
  • D.归约动作

答案:B。Yacc 的冒号就是把左侧非终结符连接到右侧候选,数学上写作 A → α,分号结束一条规则,| 表示“或”。冒号既不是赋值、比较,也不是归约动作,而是文法产生式的写代码方式。

Q7. CYK 算法要求文法呈 CNF 形式,最根本的原因是( )。

单选题 · extreme

  • A.CNF 能生成更多的语言
  • B.二元产生式恰好对应“子串划分为两段”← 正确答案
  • C.CNF 消除了所有终结符
  • D.CNF 文法一定是无二义的

答案:B。CYK 按子串长度从小到大填表,检查 string[i..j] 能否归约成 A 时,需要找切分点 k 把子串分成两段;若左半归约成 B、右半归约成 C 且存在 A → BC,则整段可归约。二元产生式与“两段子串”的对应是 CYK 高效填表的根基。CNF 不改变语言,也不消除二义性,更不会消灭终结符。

Q8. 以下哪些属于 CFG 四元组的组成部分?

多选题 · mid

  • A.变量集合 V← 正确答案
  • B.终结符集合 T← 正确答案
  • C.产生式集合 P← 正确答案
  • D.终止状态集合 F

答案:ABC。四元组是 (V, T, P, S),分别对应半成品名单、最小砖块名单、拼装指令清单和最终产品名。D 的终止状态集合 F 属于自动机五元组,不出现在 CFG 的定义里。

Q9. 文法 E → E + E | id 是二义文法,消除二义性最常用的工程手段是( )。

单选题 · mid

  • A.删掉一条产生式
  • B.声明优先级与结合性← 正确答案
  • C.把所有非终结符改成终结符
  • D.增加更多终结符

答案:B。工程实践中,Yacc 等工具允许通过 %left %right 声明运算符的优先级与结合性,让冲突自动消解。类比:车间主任不改图纸,只在手册上加一条“默认左结合”的备注。删产生式会缩小语言,C 和 D 则完全改变了文法结构。

Q10. 关于 Greibach 范式(GNF),下列哪些说法正确?

多选题 · hard

  • A.每条产生式以终结符开头← 正确答案
  • B.任意上下文无关语言都能转换为 GNF← 正确答案
  • C.GNF 只能描述正则语言
  • D.转换 GNF 通常需要先消除左递归← 正确答案

答案:ABD。GNF 的定义就是右部第一个符号必须是终结符(A 对);理论上任意 CFL 都能转换成 GNF(B 对);转换前要消除左递归,否则最左推导会陷入无限循环(D 对)。C 错在 GNF 描述的是上下文无关语言,其能力远强于正则语言——终结符开头的产生式+递归非终结符,依然能生成 aⁿbⁿ 这类非正则语言。