1. 为什么第五章的课后题值得单独拎出来聊
形式语言与自动机这门课,前四章讲的是有限状态机、正则语言、正则表达式这些相对“接地气”的东西,到了第五章,画风突然就变了。如果你翻的是那本经典的教材,第五章大概率是**上下文无关文法(Context-Free Grammar, CFG)以及下推自动机(Pushdown Automaton, PDA)**的内容。这一章是整门课的分水岭——前面你还能靠画状态图混过去,到了这里,推导、归约、栈操作、泵引理,一个比一个抽象。
我带过几轮这门课的辅导,也自己刷过不止一个版本的课后题。说实话,第五章的课后题是整本书里最容易让人卡住的地方,没有之一。原因很简单:前面的题目有“标准动作”,第五章的题目开始考验你对“形式化描述”的理解深度。比如给你一个语言,让你写文法,答案不唯一,但你必须写出一个“对”的;再比如给你一个文法,让你构造PDA,步骤是机械的,但中间任何一步搞错,整个自动机就是废的。
这篇内容就是围绕第五章课后题展开的。我会把常见的题型分类拆解,把每一类题目的解题思路、易错点、以及我自己在做题和讲题过程中总结出来的“野路子”都写出来。不管你是正在被作业折磨的学生,还是想重新捡起这块知识的从业者,应该都能找到对你有用的东西。
先说一下第五章课后题大致会覆盖哪些内容,心里有个谱:
- 上下文无关文法的设计与简化:给语言写文法、消除左递归、消除二义性、化简文法。
- 文法与语言的等价性证明:证明某个文法生成某个语言,或者反过来。
- 下推自动机的构造:给语言或文法构造PDA,包括空栈接受和终态接受两种方式。
- PDA与CFG的等价转换:从PDA到CFG,或者从CFG到PDA。
- 泵引理证明非上下文无关:用CFG的泵引理证明某个语言不是上下文无关的。
- 确定性下推自动机(DPDA)与固有歧义性:判断语言是否被DPDA接受,判断文法是否固有歧义。
下面我按题型来拆,每一类都会给出具体的解题框架和典型例题的完整思路。
2. 上下文无关文法设计题的破题套路
2.1 从语言描述到文法的“逆向思维”
课后题里最常见的一类就是:给定一个语言的自然语言描述,要求写出生成该语言的上下文无关文法。比如“写一个文法,生成所有由相同数量的a和b组成的字符串”或者“生成所有回文串”之类的。
很多同学拿到这种题的第一反应是“凭感觉写”,写出来之后自己也不知道对不对。其实这类题有非常清晰的套路,我把它叫做**“逆向思维法”**:不要从文法出发去想它生成什么,而是从目标字符串的结构出发,去想“这个字符串是怎么一步步搭出来的”。
以“相同数量的a和b”为例。这个语言是 { a^n b^n | n ≥ 0 } 吗?不是,因为a和b可以交错,比如abba、baab、aabb都算。那这个语言其实是 { w | w中a的个数等于b的个数 }。这个语言是上下文无关的,但它的文法不是那么直观。
我一般会这样引导学生思考:假设你有一个字符串,你怎么判断它a和b数量相等?一个常用的方法是“配对消除”——找到一个a和一个b,把它们消掉,剩下的字符串如果还能继续配对,最后消空,那就相等。这个过程对应到文法上,就是:
S → ε | aSb | bSa | SS这个文法怎么理解?aSb表示我在左边放一个a,右边放一个b,中间递归;bSa同理;SS表示两个合法的子串拼起来还是合法的。这个文法生成的语言恰好就是a和b数量相等的所有字符串。
但这里有个坑:这个文法是有二义性的。比如字符串ab,可以只通过aSb → ab得到,也可以通过SS → aSbS → abS → ab得到。课后题如果要求“写出一个无二义性的文法”,那这个答案就不合格。不过大多数题目只要求“写出一个文法”,不要求无二义性,所以这个答案是可以接受的。
注意:如果题目明确要求“无二义性”,那你就需要换一种思路。对于a和b数量相等的语言,无二义性的文法需要更复杂的设计,通常要引入“前缀性质”来强制唯一推导。
2.2 消除左递归的标准化操作
另一类高频题是:给定一个文法,要求消除左递归。这类题是纯机械操作,但步骤必须记牢,顺序不能乱。
直接左递归的形式是A → Aα | β,消除后的形式是:
A → βA' A' → αA' | ε间接左递归稍微麻烦一点,需要先通过代入把间接左递归变成直接左递归,然后再消除。我见过很多同学在代入这一步出错,原因是代入的顺序搞反了。正确的做法是:按照非终结符的某个顺序,依次把前面的非终结符代入到后面的产生式中,确保处理到第i个非终结符时,它的所有产生式都不以它自己或它前面的非终结符开头。
举个例子,假设有文法:
S → Aa | b A → Ac | Sd | ε这里S和A之间有间接左递归:S → Aa → Sda。消除的步骤是:
- 先把S的产生式代入A中。A → Ac | Aad | bd | ε。
- 现在A有直接左递归:A → Ac | Aad | ...。提取公因子,得到 A → (c | ad)A | bd | ε。
- 消除直接左递归:A → bdA' | A',A' → cA' | adA' | ε。
- 再把新的A代入S:S → bdA'a | A'a | b。
这个过程看起来繁琐,但每一步都是确定的。我建议做这类题的时候在草稿纸上把每一步的产生式集合都写清楚,不要跳步,跳步必错。
2.3 文法化简:去掉无用符号和ε产生式
文法化简也是第五章课后题的常客。化简的目标是得到一个“干净”的文法,不包含无用符号、ε产生式(除非语言本身包含空串)、以及单位产生式。
化简的顺序很重要,我一般推荐:
- 先消除ε产生式:找出所有可空的非终结符,然后修改产生式。
- 再消除单位产生式:A → B 这种,把B的产生式复制给A。
- 最后消除无用符号:包括无法从开始符号到达的符号,以及无法推导出终结符串的符号。
这个顺序不能乱。如果你先消除无用符号,再消除ε产生式,可能会发现原本“有用”的符号因为ε产生式的消除而变得无用了,又得重新来一遍。
实操心得:消除ε产生式的时候,最容易漏掉的是“组合情况”。比如产生式 A → BCD,如果B、C、D都可空,那么A可以推导出BC、BD、CD、B、C、D、ε等多种形式。很多同学只考虑了“全部保留”和“全部删除”两种情况,中间的组合漏掉了,导致化简后的文法生成的语言变小了。
3. 下推自动机构造题的实操细节
3.1 从文法到PDA的机械转换
课后题里有一类题是:给定一个CFG,构造等价的PDA。这类题有标准算法,我把它叫做“三状态法”:
- 创建一个开始状态q0,一个“主循环”状态q1,一个接受状态q2。
- q0到q1:压入栈底符号Z,然后压入文法的开始符号S。即 δ(q0, ε, Z) = {(q1, SZ)}。
- q1到q1:对于每个产生式 A → α,δ(q1, ε, A) = {(q1, α)}。这是“推导”步骤。
- q1到q1:对于每个终结符a,δ(q1, a, a) = {(q1, ε)}。这是“匹配”步骤。
- q1到q2:当栈顶是Z时,δ(q1, ε, Z) = {(q2, ε)}。这是“接受”步骤。
这个算法是万能的,任何CFG都可以这样转成PDA。但课后题往往不会只让你套算法,而是会加一些限制,比如“要求PDA只有一个状态”或者“要求用终态接受而不是空栈接受”。
如果要求“只有一个状态”,那就不能用上面的三状态法了。单状态PDA的构造需要把“状态”的信息编码到栈里。这种题难度较大,但思路是固定的:栈里除了存储符号,还要存储“当前处于哪个阶段”的信息。
3.2 从语言直接构造PDA:以a^n b^n为例
如果题目直接给语言让你构造PDA,而不是给文法,那就需要你直接设计栈操作。以经典语言 { a^n b^n | n ≥ 0 } 为例:
思路很直接:读a的时候往栈里压符号,读b的时候从栈里弹符号,最后栈空则接受。
具体构造(空栈接受):
- 状态:q0(读a阶段),q1(读b阶段)。
- 开始状态:q0,初始栈符号:Z。
- δ(q0, a, Z) = {(q0, AZ)}:读第一个a,压入A。
- δ(q0, a, A) = {(q0, AA)}:继续读a,继续压A。
- δ(q0, b, A) = {(q1, ε)}:读到第一个b,弹出一个A,切换到q1。
- δ(q1, b, A) = {(q1, ε)}:继续读b,继续弹A。
- δ(q1, ε, Z) = {(q1, ε)}:栈空,接受。
这个构造里,q0和q1的区分是关键。如果没有q1,那么读完b之后还可以继续读a,就会接受a^n b^n a^m这种不该接受的字符串。
注意:空栈接受的PDA不需要显式的接受状态,但需要确保在所有输入读完后栈恰好为空。如果题目要求终态接受,那就需要加一个接受状态,并且在栈空时转移到该状态。
3.3 PDA的确定性:DPDA与非DPDA的边界
课后题里有一类判断题:判断一个给定的PDA是否是确定性的。确定性的定义是:对于每个状态、每个输入符号(或ε)、每个栈顶符号,最多只有一个转移。
这里最容易踩的坑是ε转移。很多同学只检查了输入符号的转移,忽略了ε转移。如果一个状态在某个栈顶符号下既有ε转移又有输入符号转移,那它就不是确定性的。
比如:
δ(q0, a, Z) = {(q0, AZ)} δ(q0, ε, Z) = {(q1, Z)}这两个转移在栈顶为Z时同时存在,所以这个PDA是非确定性的。
但这里有个微妙的地方:非确定性PDA不一定接受非上下文无关语言。有些语言既可以被DPDA接受,也可以被NPDA接受,但NPDA的构造更简单。课后题如果问“这个语言是否被DPDA接受”,你需要判断的是语言本身的性质,而不是某个特定PDA的性质。
4. 泵引理证明题的完整拆解
4.1 CFG泵引理的表述与理解
CFG的泵引理是第五章最让人头疼的内容之一。它的表述是:如果L是上下文无关语言,那么存在一个常数p,使得对于L中任何长度≥p的字符串w,都可以写成w = uvxyz,满足:
- |vxy| ≤ p
- |vy| ≥ 1
- 对于所有i ≥ 0,uv^i xy^i z ∈ L
这个引理看起来和正则语言的泵引理很像,但多了v和y两个部分。理解它的关键是:v和y是被“泵”的部分,x是中间不变的部分,u和z是两边不变的部分。
为什么CFG的泵引理需要五个部分?因为CFG的推导树比正则语言的自动机更深。在推导树中,如果字符串足够长,那么从根到叶子的路径上一定有两个相同的非终结符。这两个非终结符之间的部分可以重复任意次,对应到字符串上就是v和y可以同时重复。
4.2 用泵引理证明非上下文无关的实战步骤
用泵引理证明一个语言不是上下文无关的,标准步骤是:
- 假设L是上下文无关的,取泵引理常数p。
- 选择一个长度≥p的字符串w ∈ L。这个选择很关键,要选得“巧妙”,让后面的分析容易导出矛盾。
- 考虑w的所有可能分解w = uvxyz,其中|vxy| ≤ p且|vy| ≥ 1。
- 对于每种分解,找到一个i,使得uv^i xy^i z ∉ L。
- 导出矛盾,证明假设不成立。
以经典语言 L = { a^n b^n c^n | n ≥ 0 } 为例:
假设L是上下文无关的,取常数p。选择w = a^p b^p c^p。
因为|vxy| ≤ p,所以vxy不能同时包含a、b、c三种符号。它最多包含两种符号。
- 如果vxy只包含a和b,那么泵之后a和b的数量会变化,但c的数量不变,导致三者数量不等。
- 如果vxy只包含b和c,同理。
- 如果vxy只包含a,那么泵之后a的数量变化,b和c不变。
- 如果vxy只包含b或c,同理。
无论哪种情况,泵之后字符串都不在L中。矛盾。
所以L不是上下文无关的。
这个证明看起来简单,但实际操作中有几个坑:
坑一:w的选择。如果你选w = a^p b^p c^p,但p不够大,vxy可能跨越三种符号。但因为|vxy| ≤ p,而每种符号至少有p个,所以vxy不可能同时包含三种符号。这个推理要写清楚。
坑二:i的选择。通常i = 0或i = 2就够了。i = 0是“抽掉”v和y,i = 2是“加倍”。大多数题目用i = 0就能导出矛盾。
坑三:分解的穷举。有些同学只考虑了vxy在a区域的情况,忽略了vxy跨越a和b边界的情况。虽然最终结论一样,但证明过程不完整会被扣分。
4.3 泵引理证明的常见变体
课后题里还有一些变体,比如:
- 证明 { a^i b^j c^k | i < j < k } 不是上下文无关的:这个语言比a^n b^n c^n更复杂,但用泵引理依然可以证明。选择w = a^p b^{p+1} c^{p+2},然后分析vxy的位置。
- 证明 { ww | w ∈ {a,b}} 不是上下文无关的*:这个语言是“字符串重复”,选择w = a^p b^p a^p b^p,然后分析vxy的位置。
- 证明 { a^n b^n c^m | n ≠ m } 不是上下文无关的:这个语言看起来“几乎”是上下文无关的,但n ≠ m这个条件让它变成了非上下文无关。证明时需要小心选择w。
这些变体的证明思路是一样的,但选择的字符串不同。我建议做这类题的时候先把目标语言的性质分析清楚,然后选择一个“最能暴露矛盾”的字符串。
5. 常见问题与排查技巧实录
5.1 文法设计题:写出来的文法“太强”或“太弱”
这是最常见的问题。写出来的文法要么生成了不该生成的字符串(太强),要么没有生成所有该生成的字符串(太弱)。
排查方法:用边界情况测试。对于你写出的文法,手动推导几个短字符串,看看是否在目标语言中。比如对于“a和b数量相等”的语言,测试ε、ab、ba、aabb、abab、abba、baab、baba。如果这些都能生成,再测试a、b、aab、abb这些不该生成的,看看文法是否能生成它们。
如果文法太强,通常是某个产生式的“自由度”太大了。比如S → aS | Sb | ε这个文法生成的是所有字符串(因为你可以任意插入a和b),而不是a和b数量相等的字符串。修正的方法是引入“配对”机制,比如S → aSb | bSa | SS | ε。
如果文法太弱,通常是漏掉了某些“组合情况”。比如S → aSb | ε只生成a^n b^n,漏掉了abba这种交错的情况。修正的方法是加入SS或者bSa这样的产生式。
5.2 PDA构造题:栈操作“提前”或“滞后”
PDA构造题最常见的错误是栈操作的时机不对。比如在a^n b^n的PDA中,如果读b的时候没有及时切换状态,就会接受a^n b^n a^m这种字符串。
排查方法:画出栈的变化序列。对于输入字符串aabb,手动模拟PDA的每一步,看看栈的内容是否正确。如果栈在读完所有输入后不为空,或者在不该空的时候空了,那就是栈操作有问题。
另一个常见问题是ε转移的滥用。有些同学为了让PDA“更灵活”,加了大量的ε转移,结果导致PDA变成非确定性的,或者接受了不该接受的字符串。我的建议是:能用输入符号转移就不用ε转移,ε转移只在必要的时候使用(比如从读a阶段切换到读b阶段)。
5.3 泵引理证明题:分解情况“漏了”或“重了”
泵引理证明题最常见的问题是分解情况考虑不全。比如对于w = a^p b^p c^p,vxy可能的位置有:
- 完全在a区域
- 跨越a和b的边界
- 完全在b区域
- 跨越b和c的边界
- 完全在c区域
很多同学只考虑了“完全在某个区域”的情况,漏掉了“跨越边界”的情况。虽然最终结论一样,但证明过程不完整。
排查方法:画出字符串的“分区图”,把w分成几个区域,然后逐一分析vxy可能落在哪些区域。对于每种情况,选择一个i导出矛盾。
5.4 常见问题速查表
| 问题类型 | 典型症状 | 排查方法 | 修正思路 |
|---|---|---|---|
| 文法太强 | 生成了不该生成的字符串 | 用短字符串测试 | 减少产生式的自由度,加入配对机制 |
| 文法太弱 | 漏掉了该生成的字符串 | 用边界情况测试 | 加入组合产生式,如SS |
| PDA栈操作错误 | 接受不该接受的字符串 | 手动模拟栈变化 | 调整状态切换时机,减少ε转移 |
| 泵引理分解不全 | 证明过程被扣分 | 画出分区图 | 逐一分析vxy的所有可能位置 |
| 消除左递归出错 | 代入顺序混乱 | 按非终结符顺序逐步代入 | 不要跳步,每步写清楚 |
| 消除ε产生式漏组合 | 化简后语言变小 | 检查所有可空非终结符的组合 | 考虑部分删除的情况 |
5.5 独家避坑技巧
技巧一:文法设计先写“骨架”再填“血肉”。对于复杂的语言,先写出最核心的产生式(比如a^n b^n的骨架是S → aSb | ε),然后再考虑如何扩展(比如加入交错情况)。
技巧二:PDA构造先确定“阶段”再确定“栈操作”。把PDA的工作过程分成几个阶段(比如“读a阶段”“读b阶段”“接受阶段”),每个阶段对应一个状态,然后再设计每个状态下的栈操作。
技巧三:泵引理证明先选“最坏情况”的字符串。选择的字符串要让vxy无论怎么放都会导致矛盾。通常选择“三种符号各p个”或者“两种符号交替p次”这样的字符串。
技巧四:化简文法时用“标记法”。在草稿纸上把每个非终结符标记为“可空”“可达”“可终结”,然后逐步消除。不要凭感觉删符号。
技巧五:做完题后“反向验证”。对于文法题,用文法推导几个字符串,看看是否在目标语言中;对于PDA题,手动模拟几个字符串,看看是否被正确接受或拒绝。这个习惯能帮你发现大部分错误。
6. 从课后题到实际应用的思维迁移
6.1 为什么这些“抽象”的题目值得认真做
很多同学做第五章课后题的时候会想:“这些东西有什么用?实际工作中又不会让我写文法。”这个想法可以理解,但忽略了一个重要的事实:上下文无关文法和下推自动机是编译器、解释器、以及各种语言处理工具的理论基础。
你写的每一个解析器(parser),背后都是CFG在支撑。你用的每一个正则表达式引擎,虽然主要是正则语言,但在处理嵌套结构(比如括号匹配)时,就需要CFG的能力。你设计的每一个协议解析器,如果协议有嵌套结构,那本质上就是在处理上下文无关语言。
课后题里的那些“写文法”“构造PDA”“用泵引理证明”,看起来是纯理论,但它们训练的是形式化描述能力和边界情况分析能力。这两种能力在实际工作中非常值钱。
6.2 从课后题到解析器设计的映射
以“写一个文法生成所有平衡括号串”为例。这个课后题的答案很简单:
S → (S) | SS | ε但在实际应用中,这个文法对应的是一个括号匹配解析器。如果你要写一个JSON解析器,JSON的语法本质上就是一个CFG。你要处理的嵌套对象、嵌套数组,都是通过递归下降或者LR分析来处理的。
再比如,课后题里让你“消除左递归”,这个操作在实际的递归下降解析器中是必须的。因为递归下降解析器不能处理左递归文法,否则会无限递归。所以你在设计解析器之前,必须先把文法转换成无左递归的形式。
6.3 从PDA到栈式虚拟机的映射
PDA的栈操作和栈式虚拟机(比如JVM、Python虚拟机)的执行模型非常相似。PDA的“压栈”“弹栈”对应虚拟机的“push”“pop”指令。PDA的“状态转移”对应虚拟机的“指令跳转”。
如果你理解了PDA的工作原理,那么理解栈式虚拟机的执行模型就会容易很多。反过来,如果你在实际工作中接触过栈式虚拟机,那么做PDA的课后题也会更有感觉。
6.4 从泵引理到“不可能性证明”的思维训练
泵引理证明题训练的是“证明某件事不可能”的能力。这种能力在实际工作中同样重要。比如,当你需要证明“某个协议无法在有限状态下实现”或者“某个语言无法用正则表达式描述”时,泵引理就是你的工具。
虽然实际工作中很少直接用到泵引理,但这种“找到反例”的思维方式是非常有价值的。它让你在面对复杂问题时,能够快速判断“这个问题有没有解”“这个方案有没有根本性的缺陷”。
7. 我个人的做题节奏与复盘方法
7.1 做题节奏:先分类,再逐个击破
我自己的习惯是:拿到第五章的课后题,先花10分钟把题目分类。哪些是文法设计题,哪些是PDA构造题,哪些是泵引理证明题,哪些是化简题。分类之后,按照“先易后难”的顺序做。
通常的顺序是:化简题 → 文法设计题 → PDA构造题 → 泵引理证明题。化简题是机械操作,做起来快,能帮你进入状态。文法设计题需要一些创造力,但难度可控。PDA构造题步骤多,需要细心。泵引理证明题最难,放在最后做。
7.2 复盘方法:错题本 + 模式识别
我强烈建议准备一个错题本。不是简单地抄题目和答案,而是记录错误的原因和正确的思路。比如:
- 错误原因:消除左递归时代入顺序搞反了。
- 正确思路:按照非终结符的顺序,依次把前面的代入后面的。
- 类似题目:第5.3题、第5.7题。
这样积累下来,你会发现自己的错误是有模式的。比如你可能总是在“ε产生式的组合情况”上出错,或者在“泵引理的分解情况”上漏掉边界。识别出这些模式之后,就可以有针对性地练习。
7.3 时间分配建议
如果你有10道第五章的课后题,我建议的时间分配是:
- 化简题(2道):30分钟
- 文法设计题(3道):60分钟
- PDA构造题(3道):60分钟
- 泵引理证明题(2道):60分钟
- 复盘和检查:30分钟
总共大约4小时。如果你发现某类题目花的时间远超预期,那说明这类题目是你的薄弱环节,需要额外练习。
最后再分享一个小技巧:做PDA构造题的时候,用纸笔画栈的变化序列,比在脑子里想要可靠得多。我见过太多同学因为“心算”栈操作而犯错。画出来,一步一步走,虽然慢一点,但正确率高很多。
7.4 关于“答案不唯一”的心态调整
第五章的很多题目答案不唯一。文法设计题可以写出多个正确的文法,PDA构造题也可以有不同的状态设计。这会让很多同学习惯性地怀疑“我的答案是不是不对”。
我的建议是:只要你的答案满足题目的要求,就是对的。不要因为和参考答案不一样就否定自己。当然,前提是你需要验证自己的答案确实满足要求。验证的方法就是前面说的“边界情况测试”和“手动模拟”。
如果你实在不确定,可以把你的答案和同学的对比,看看生成的语言是否一致。如果一致,那就没问题。如果不一致,再分析谁对谁错。
7.5 关于“泵引理”的心理障碍
泵引理是很多同学的心理障碍。一看到“证明不是上下文无关”就慌了。其实泵引理的证明套路非常固定:假设 → 选字符串 → 分析分解 → 导出矛盾。你只需要把每一步写清楚,不要跳步,就能拿到大部分分数。
我见过很多同学在泵引理证明题上得零分,不是因为不会,而是因为写得太简略。比如只写了“无论怎么分解都会导致矛盾”,但没有具体分析每种分解情况。这种答案在考试中会被扣分。所以,宁可写得多一点,也不要跳步。
7.6 从“做题”到“出题”的进阶
如果你已经把第五章的课后题刷完了,想进一步提升,我建议你尝试“自己出题”。比如:
- 给定一个语言,写出它的文法,然后构造PDA,然后用泵引理证明它不是正则的(如果适用)。
- 给定一个文法,消除左递归,化简,然后构造PDA。
- 给定一个PDA,写出它的文法,然后判断它是否是确定性的。
这种“自己出题”的练习能帮你把知识点串联起来,形成完整的知识网络。而且,当你能够自己出题的时候,你就真正掌握了这部分内容。
7.7 关于教材选择的补充
不同教材的第五章内容可能略有差异。有的教材把CFG和PDA放在第五章,有的放在第六章。有的教材把泵引理放在更靠后的章节。如果你发现自己的教材和这篇内容的章节划分不一致,不用慌,核心知识点是一样的:CFG、PDA、泵引理、化简、消除左递归。你只需要根据自己教材的章节安排,找到对应的内容即可。
如果你用的是那本经典的“龙书”(编译原理),第五章可能是语法分析的内容。但形式语言与自动机这门课的教材通常是另一本,比如那本经典的“乌龟书”(Introduction to the Theory of Computation)。不同教材的侧重点不同,但课后题的题型是相似的。
7.8 最后的一点个人体会
形式语言与自动机这门课,第五章是最“劝退”的一章。但也是最能锻炼“形式化思维”的一章。我自己的体会是:这一章的课后题,做一遍是不够的。第一遍做的时候,你是在“模仿”解题套路;第二遍做的时候,你是在“理解”背后的原理;第三遍做的时候,你才能“灵活运用”。
如果你现在正在被第五章的课后题折磨,我想说:这是正常的。几乎每个人都会在这个阶段卡住。但只要你坚持把每一类题目的套路搞清楚,把每一个易错点记下来,你最终会发现,这些题目其实并没有那么难。它们只是需要你换一种思维方式——从“直觉”转向“形式化”。
而这种思维方式的转变,才是这门课真正想教给你的东西。