简介:离散数学西电第三版配套教材答案,面向计算机、软件工程、人工智能等专业学生,帮助解决课后习题无标准参考、自学难以判断解题对错的问题。资源为单个PDF文件,压缩包大小约9.26MB,内容按教材章节顺序编排,完整覆盖集合论、关系与函数、图论、组合数学、数理逻辑等核心模块,每道题均给出关键步骤或证明过程,便于快速定位与对照。目前已有400人学习下载,适合正在使用该教材的读者用于日常作业订正、考研或期末考试前系统刷题,也可供任课教师布置作业或答疑时参考。借助这份答案,读者可以比对自身解题思路,识别并纠正易错点,深入理解定义、定理与典型题目之间的联系,有效提升离散数学的复习效率。
1. 离散数学西电第三版:把课后答案当成推理链的断点调试器
大多数人刷离散数学时,习惯先看题目再翻答案,看到结果一致就跳过,不一致就背下来。这样做的损失很大:答案里的每一步推导,其实是“证明的断点”,就像调试器里的堆栈信息。离散数学西电第三版从集合论、关系与函数、图论、组合数学到数理逻辑,每个章节的习题都围绕定理的应用展开,参考解答则展示了教材作者如何组织这些定理。对计算机方向的学生和写算法的人,这本教材值得精读,但姿势要换:用程序去验证数学结论,再回头对照答案,找出是哪一步推理断了。这篇文章就按这个思路,把每一章的关键内容和自查方法拆开讲。
2. 数理逻辑与集合论:用真值表和集合运算验证课后答案
命题逻辑这一章的习题,最常用的技巧是把自然语言翻译成命题公式,再用真值表或等值演算判断永真性。很多同学做完后只看最后的真/假,忽略公式变形过程。实际上,课后答案的价值在于展示每一步用了哪条等价式,例如吸收律、德摩根律、蕴含等值式。更好的做法是自己先在纸上完成推导,再写一小段真值表程序对结论做独立验证。
2.1 用穷举真值表验证论证有效性
考虑教材中最常见的论证形式:由若干前提推出一个结论。一个论证有效当且仅当不存在一组真值赋值让所有前提为真而结论为假。手动列真值表容易漏行,尤其命题变元超过两个时。用 Python 的 product 枚举全部赋值,可以快速把“有效/无效”的判断变成一个布尔函数。
from itertools import product
def validate(premises, conclusion):
"""premises 和 conclusion 都是接收 p,q,r 返回布尔的函数。"""
for p, q, r in product([True, False], repeat=3):
if all(cond(p, q, r) for cond in premises) and not conclusion(p, q, r):
return False
return True
premises = [
lambda p, q, r: (not p) or q, # p -> q
lambda p, q, r: (not q) or r, # q -> r
]
print(validate(premises, lambda p, q, r: (not p) or r))
product([True, False], repeat=3) 枚举 p、q、r 的全部 8 种赋值组合; all(cond(p, q, r) for cond in premises) 判断当前赋值下所有前提是否为真; not conclusion(p, q, r) 检查结论是否为假。如果同时满足这两个条件,说明论证无效,返回 False。上面的例子对应三段论,返回 True。若你的结果和教材答案不一致,可以把 return 前加一行 print(p, q, r) ,直接打印出反例赋值,会比重新翻书更快找到错在哪一步。这个版本预设了三个命题变元 p、q、r;如果题目有四到五个变元,把 repeat 和 lambda 参数列表同步扩展即可。
| 逻辑联结词 | 教材写法 | Python 表达式 |
|---|---|---|
| 否定 | $\neg p$ | not p |
| 合取 | $p \wedge q$ | p and q |
| 析取 | $p \vee q$ | p or q |
| 蕴含 | $p \to q$ | not p or q |
| 等值 | $p \leftrightarrow q$ | (p and q) or (not p and not q) |
实际做题时,教材答案常用主析取范式来判定永真性,而穷举真值表本质上是在做同一个集合的遍历。两者对同一个公式的结果必然一致,所以程序输出和答案冲突时,优先检查自己有没有把蕴含翻译错,而不是怀疑代码。
2.2 谓词逻辑的量词顺序:用有限域反例识别错解
谓词逻辑比命题逻辑更容易出错,主要原因是量词嵌套顺序。教材中的“对于任意 x,存在 y,使 P(x, y) 成立”与“存在 y,对任意 x,使 P(x, y) 成立”看起来相似,但逻辑强度完全不同。课后答案里经常出现一处典型错误:把 ∀x∃yP(x,y) 直接当成 ∃y∀xP(x,y) ,在后续证明中默认存在一个统一的 y。
domain = [1, 2, 3]
def P(x, y):
return x + y == 4
all_exists = all(any(P(x, y) for y in domain) for x in domain)
exists_all = any(all(P(x, y) for x in domain) for y in domain)
print("∀x ∃y:", all_exists) # True
print("∃y ∀x:", exists_all) # False
第一个表达式 all(any(...)) 对每个 x 判断“是否存在 y”,用 any 嵌套在 all 中;第二个表达式 any(all(...)) 则反过来,先看是否存在某个 y 能让所有 x 成立。运行结果会清楚显示两个命题的真值不同。遇到答案里出现量词换序的步骤,就把论域换成一个三元素集合跑一遍,能快速判断该步是否成立。这里尤其要注意空论域:如果 domain 为空, all(...) 会返回 True,而 any(...) 会返回 False,很多推导错误在空论域下会被掩盖。
2.3 集合恒等式:用 Python 的 set 类型做机械验证
集合论习题常见要求是证明两个集合相等。手写证明通常用“若 x∈左边,则 x∈右边”的双包含法。这类证明的每一步都需要依赖交、并、补的定义,错一步整体就会偏。对应的机械验证却很便宜:用 Python 内置的 set 类型,对有限集合直接判断两个表达式是否相等。
A = {1, 2, 3}
B = {2, 3, 4}
left = (A | B) - (A & B)
right = (A - B) | (B - A)
print(left == right) # True,对称差两种定义等价
| 对应并集, - 对应集合差, & 对应交集, == 判断两个集合是否相等。这条式子对应教材里的 A ⊕ B = (A ∪ B) - (A ∩ B) ,与 (A - B) ∪ (B - A) 等价。遇到规模较大的集合,手写双包含证明太耗时,先调用 set 运算确认结论是否为真,再回头补证明思路,效率高很多。
| 集合论概念 | 教材记号 | Python 写法 | 典型课后题场景 |
|---|---|---|---|
| 并集 | $A \cup B$ | A | B | 两个条件的并集 |
| 交集 | $A \cap B$ | A & B | 同时满足两个条件 |
| 差集 | $A - B$ | A - B | 排除法计数 |
| 对称差 | $A \oplus B$ | (A | B) - (A & B) | 恰好满足一个条件 |
| 幂集 | $\mathcal{P}(A)$ | 见下方 power_set | 子集计数 |
def power_set(S):
elems = list(S)
result = [set()]
for e in elems:
result.extend([sub | {e} for sub in result])
return result
print(len(power_set({1, 2, 3}))) # 8
power_set 每次迭代取出一个元素,对已有子集集合中的每个子集生成加入该元素的新子集,与教材中“含有 n 个元素的集合幂集大小为 2^n”的归纳证明对应。这可以验证答案中的幂集是否列全。使用 set 做验证时要注意,Python 集合的元素必须是可哈希的,列表、集合这些可变类型不能直接放进集合。教材习题中的元素通常是整数或符号,问题不大;如果遇到元素是结构体,先用 tuple 包装再放入 set,否则会报 TypeError: unhashable type 。
3. 关系与函数:Warshall 闭包算法与函数性质的代码复现
关系和函数这一章是图论与数据库理论的共同基础。课后习题经常包含三块:关系矩阵运算、计算传递闭包、判断函数性质。手工计算关系闭包时,三步并作两步非常常见,尤其是复合关系需要连续查矩阵。下面的做法是把关系矩阵当作二维数组,用代码执行布尔运算,输出结果再与答案对照。
3.1 关系矩阵的复合运算
关系合成 $R \circ S$ 的定义是:$(a,b) \in R \circ S$ 当且仅当存在中间元素 $c$ 使 $(a,c) \in R$ 且 $(c,b) \in S$。在矩阵中,这相当于布尔矩阵乘法。理解这一点后,答案中复杂的路径枚举就变成了一个三重循环。
def compose(R, S):
n = len(R)
result = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
result[i][j] = any(R[i][k] and S[k][j] for k in range(n))
return result
R = [[1, 1, 0],
[0, 1, 1],
[1, 0, 0]]
S = [[0, 1, 0],
[1, 0, 1],
[0, 1, 1]]
print(compose(R, S))
any(...) 对 k 从 0 到 n-1 判断是否存在一条中间链路; R[i][k] and S[k][j] 对应关系矩阵中第 i 行第 k 列与第 k 行第 j 列同时为 1。这里 i、j、k 分别代表前件、后件与中间元素的下标。对照课后答案时,把计算出的矩阵一行一行读出来,能很快发现自己在手工复合时漏掉了哪条中间路径。如果关系矩阵不是方阵, compose 函数里的 n = len(R) 就可能越界。关系合成要求 R 的列数等于 S 的行数,实际使用时应增加列数检查: assert len(R[0]) == len(S) 。教材中的合成题通常默认同集上的关系,矩阵为方阵,所以省略了这一步,但自己扩展时要注意。
| 关系运算 | 矩阵视角 | 代码结构 | 典型答案场景 |
|---|---|---|---|
| 并 | 逐位 OR | R[i][j] or S[i][j] | 合并两个关系 |
| 交 | 逐位 AND | R[i][j] and S[i][j] | 同时满足 |
| 合成 | 布尔矩阵乘法 | any(R[i][k] and S[k][j] for k in range(n)) | 间接关联 |
| 转置 | 行列互换 | S[j][i] = R[i][j] | 逆关系 |
3.2 用 Warshall 算法计算传递闭包
传递闭包的定义是最小传递关系包含原关系。手工求闭包时不断补齐可达路径,容易补过头或漏掉。Warshall 算法按中间点顺序更新矩阵,是一个稳定的计算过程,也适合作为教材答案的核对工具。
def warshall(R):
n = len(R)
T = [row[:] for row in R]
for k in range(n):
for i in range(n):
if T[i][k]:
for j in range(n):
T[i][j] = T[i][j] or T[k][j]
return T
R = [[1, 1, 0],
[0, 1, 1],
[1, 0, 0]]
print(warshall(R))
参数 R 是 n 阶关系矩阵; T = [row[:] for row in R] 做深拷贝,避免修改原始矩阵;外层 k 是当前允许作为中间点的顶点编号,内层更新规则是若 i 到 k 可达,则把 k 到 j 的可达性传递给 i 到 j。每次打印中间矩阵可以看到闭包的逐步变化,这些中间矩阵恰好对应教材答案中分步计算过程的每一步。如果最后一步输出与答案不一致,问题通常出在集合划分时少了一个元素,或者把 k 循环写到了最内层,导致中间点信息没有充分传播。
3.3 函数单射、满射与双射的判定
函数部分的习题一般给出从 A 到 B 的映射,要求判断是否是单射、满射或双射。手工判断时容易漏掉某个元素的像是否重复,用集合和 list 的配合可以一次性给出所有结论。
def classify_function(f, domain, codomain):
images = [f(x) for x in domain]
is_injective = len(images) == len(set(images))
is_surjective = set(images) == set(codomain)
print("单射:", is_injective)
print("满射:", is_surjective)
print("双射:", is_injective and is_surjective)
def f(x):
return x * 2 % 5
classify_function(f, range(5), range(5))
images 保存定义域所有元素的像; len(images) == len(set(images)) 通过像集合去重后长度不变来判断是否存在两个定义域元素映射到同一个像; set(images) == set(codomain) 判断像集是否覆盖整个陪域。 x * 2 % 5 这个映射在模 5 意义下是双射,输出三项全为 True。如果课后答案是双射,而你的程序输出 False,检查是否把陪域写成了其他集合。注意 classify_function 要求 domain 和 codomain 是可迭代的集合或列表,常见误用是传入整数,导致 for x in domain 报 TypeError: 'int' object is not iterable 。
4. 图论与组合数学:遍历算法与计数模型驱动习题自测
图论和组合数学是后续算法课的两条主线。教材中的图论习题多围绕路径、回路、连通性展开,组合数学则聚焦排列、组合、递推关系。这两部分的答案往往只有最终数字或路径,缺少计算过程,正好适合用程序补齐中间步骤。
4.1 用 BFS 验证图中的最短路径答案
图的最短路径问题在教材中主要以无权图形式出现,BFS 天然返回最短路径。课后答案如果给出某两点间的路径长度为 3,你可以用 BFS 确认是否还存在更短路径。
from collections import deque
def bfs_shortest_path(graph, start, target):
queue = deque([(start, [start])])
visited = {start}
while queue:
node, path = queue.popleft()
if node == target:
return path
for neighbor in graph.get(node, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None
graph = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'D'],
'D': ['B', 'C']
}
print(bfs_shortest_path(graph, 'A', 'D'))
queue 中存储 (当前节点, 到达路径) 二元组; visited 记录已经访问过的节点,避免重复入队;BFS 按层扩展,第一个到达 target 的路径就是最短路径。该代码输出的 ['A', 'B', 'D'] 或 ['A', 'C', 'D'] 长度均为 2,可以用来校正教材答案中“A 到 D 的最短路径长度为 2”的结论。如果答案给出的路径更长,说明写答案的人在推演时绕了远路,或者题目图结构与此处不同。如果图很大,队列用 deque 而不是 list 的 pop(0) ,后者是 O(n) 的操作,会在几万顶点的图上明显变慢。
4.2 欧拉回路条件:连通性与偶数度的组合检查
欧拉回路的判定是课后习题常客。教材给出的定理是:无向图存在欧拉回路当且仅当每个非零度顶点的度数为偶数,且所有非零度顶点在同一个连通分量中。很多同学只检查度数,忘了连通性,导致把两个分离环组成的图误判为欧拉图。
def is_connected(adj):
nodes = [v for v, nbs in adj.items() if nbs]
if not nodes:
return True
stack = [nodes[0]]
seen = set(stack)
while stack:
v = stack.pop()
for nxt in adj[v]:
if nxt not in seen:
seen.add(nxt)
stack.append(nxt)
return all(v in seen for v in nodes)
def has_euler_circuit(adj):
return is_connected(adj) and all(len(nbs) % 2 == 0 for nbs in adj.values())
adj = {
'A': ['B', 'C'],
'B': ['A', 'C'],
'C': ['A', 'B']
}
print(has_euler_circuit(adj)) # True
adj 是邻接表, is_connected 先收集所有非零度顶点,再用 DFS 从一个顶点出发标记可达顶点,最后检查是否全部可达; has_euler_circuit 用 all(len(nbs) % 2 == 0 for nbs in adj.values()) 检查所有顶点度数是否为偶数。这里把连通性检查和度数检查放在同一条 return 中,任何一项不成立立即返回 False。如果课后答案判断某图为欧拉图,程序输出 False,先去检查是否存在孤立边,或者某个顶点的邻接表漏填了一条边。对于有向图的欧拉回路,判定条件还要增加“每个顶点入度等于出度”,这是另一个话题。
4.3 组合计数:用递推公式和暴力枚举交叉验证
组合数学的答案常常是一个数字,比如错排数 D(4)=9。直接看答案只能知道“错没错”,看不到漏算的是哪一种排列。更可靠的做法是用递推公式手算一遍,再写一段暴力枚举代码核对小规模结果。
import itertools
def count_derangement(n):
count = 0
for perm in itertools.permutations(range(n)):
if all(perm[i] != i for i in range(n)):
count += 1
return count
def derangement(n):
a, b = 1, 0
for i in range(2, n + 1):
a, b = b, (i - 1) * (a + b)
return b
for n in range(1, 6):
print(n, count_derangement(n), derangement(n))
count_derangement 用 itertools.permutations 生成 n 个元素的全排列,再逐一检查每个位置是否都不在原位; derangement 使用迭代公式 D(n)=(n-1)(D(n-1)+D(n-2)) ,只保留前两项节省空间。两个函数输出序列从 n=1 到 5 应该完全一致:0、1、2、9、44。当 n 超过 8 时暴力枚举会明显变慢,这时只依靠递推公式并把公式推导写在答案旁边即可。组合计数中另一个容易混淆的是组合数 C(n,k) 与二项式系数。教材中有时直接用杨辉三角递推,代码实现时可写 C[n][k] = C[n-1][k-1] + C[n-1][k] ,初值 C[n][0]=C[n][n]=1 。暴力枚举适合验证小规模值,递推适合大数值计算,两者配合能避免答案只给一个数字带来的盲区。
| 组合问题类型 | 判断关键词 | 常用模型 | 小规模验证方式 |
|---|---|---|---|
| 排列 | “顺序相关” | P(n,k) | itertools.permutations |
| 组合 | “顺序无关” | C(n,k) | itertools.combinations |
| 错排 | “都不在原来位置” | D(n) | all(perm[i] != i for i in range(n)) |
| 二项式系数 | “选择 k 个” | C(n,k) | 杨辉三角递推 |
5. 用答案反向构造学习闭环:错题标注与验证时间盒
到这里,数理逻辑、集合论、关系、函数、图论和组合数学的主要题型都已经有了代码验证思路。真正提高学习效率的不是把答案从头到尾抄一遍,而是把答案当作测试用例,反推自己的推理链是否完整。
5.1 给错题打结构化标签
每次对照答案后,不要只记录“错了”,而是记录错误的类型。我常用的标签体系只有三类:概念错误、量词/条件错误、运算顺序错误。这三个标签基本能覆盖离散数学 80% 的失分点。例如被第 2 节的量词顺序坑了,就记 量词顺序错误 ;在 Warshall 算法中只做了外层循环忘记更新中间点,就记 运算顺序错误 。标签可以手工维护,也可以用下面的小脚本统计。
labels = ["概念错误", "量词顺序错误", "运算顺序错误", "概念错误"]
from collections import Counter
counter = Counter(labels)
for item in counter.most_common():
print(item)
把每次做错的标签追加到列表中,最后用 Counter.most_common() 排序输出,复习时优先看频率最高的类型。这是从课后答案反推出来的个人薄弱点清单,比机械翻书更有指导性。
5.2 用时间盒控制单题验证节奏
离散数学的习题有些需要长时间思考,但考试不会给你半小时。我的做法是给每道题分配一个时间盒:第一遍 15 分钟想不出完整证明,就先标记为待攻克,参考答案后把证明的关键步骤抄在题目旁边,再用代码复现结果。时间盒的作用是防止在一道小题上耗尽精力,导致后续章节没有时间做程序验证。对 5 年以上的从业者,这个做法尤其有效,因为工作后再看数学书,最重要的是快速重建知识和定位薄弱点,而不是在生僻证明上原地踏步。结合第 2 章到第 4 章的代码模板,每个章节的验证时间可以控制在 10 分钟以内。
下一次拿起这本书时,选一道你上次做错的关系闭包题,先在纸上手推矩阵,再用第 3 章的 warshall 函数输出每一步的中间矩阵,最后打开课后答案逐一比对从第 k 步到第 k+1 步之间发生了什么。
134



被折叠的 条评论
为什么被折叠?



