Files
maths-cs-ai-compendium-zh/chapter 13: computing and OS/01. discrete maths.md
T
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

254 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 离散数学
*离散数学是关于可数、分离结构的数学,是计算构建的基础。本文涵盖命题逻辑与谓词逻辑、证明技巧、集合、关系、函数、图论基础以及递推关系。*
- 在前面的章节中,我们研究了连续数学:微积分(第3章)、概率分布(第5章)以及实值参数的优化(第6章)。但计算机本质上是**离散**机器。它们存储比特(0或1),处理整数,遵循分支逻辑,并操作有限数据结构。**离散数学**提供了推理这些结构的形式化语言。
- 本章所有内容都建立在离散数学之上:处理器逻辑门是布尔代数,调度算法需要正确性证明,内存管理使用集合运算,算法分析需要递推关系。
## 命题逻辑
- **命题逻辑**是真假语句的代数。一个**命题**是一个要么为真(T)要么为假(F)的陈述,绝不会两者兼有。"天在下雨"是一个命题。"现在几点了?"则不是(它是一个问句,不是具有真值的陈述)。
- 命题可以通过**逻辑连接词**进行组合:
- **与**(合取,$p \wedge q$):仅当$p$和$q$都为真时为真。
- **或**(析取,$p \vee q$):当$p$或$q$至少一个为真时为真。
- **非**(否定,$\neg p$):翻转真值。
- **蕴含**(蕴涵,$p \to q$):仅当$p$为真且$q$为假时为假。"如果下雨,地就是湿的"只有在下了雨而地却是干的时候才被违反。
- **当且仅当**(双条件,$p \leftrightarrow q$):当两者真值相同时为真。
- **真值表**穷举列出所有可能的输入组合及相应的输出。对于$n$个命题,该表有$2^n$行。这就是我们验证逻辑等价性的方式:
| $p$ | $q$ | $p \wedge q$ | $p \vee q$ | $p \to q$ |
|-----|-----|--------------|------------|-----------|
| T | T | T | T | T |
| T | F | F | T | F |
| F | T | F | T | T |
| F | F | F | F | T |
- 蕴含行中$p$为假的情况值得关注:$F \to q$无论$q$为何值都为真。这就是**空真**。"如果猪会飞,那我就是英国国王"在逻辑上为真,因为前提为假。这看起来违反直觉,但对数学推理至关重要。
- **逻辑等价式**是对所有真值都成立的恒等式:
- **德摩根定律**$\neg(p \wedge q) \equiv \neg p \vee \neg q$ 和 $\neg(p \vee q) \equiv \neg p \wedge \neg q$。要否定一个AND,分别否定每个部分并切换为OR(反之亦然)。这些直接出现在编程中:`!(a && b)` 等价于 `(!a || !b)`
- **逆否命题**$p \to q \equiv \neg q \to \neg p$。"如果下雨,地就是湿的"等价于"如果地不是湿的,那么就没下雨。"这是一个强大的证明技巧。
- **双重否定**$\neg(\neg p) \equiv p$。
- **分配律**$p \wedge (q \vee r) \equiv (p \wedge q) \vee (p \wedge r)$。
- 一个总是为真(对所有真值指派)的公式是**重言式**。总是为假的公式是**矛盾式**。有时真有时假的公式是**偶然式**。例如,$p \vee \neg p$是重言式,$p \wedge \neg p$是矛盾式。
## 谓词逻辑与量词
- 命题逻辑无法表达关于集合中*所有*或*某些*元素的陈述。"每个大于2的素数都是奇数"需要**谓词逻辑**,它用变量、谓词和量词扩展了命题逻辑。
- **谓词**是依赖于变量的陈述:$P(x)$ = "$x$是偶数。"当给定$x$一个具体值时,它成为一个命题:$P(4)$为真,$P(7)$为假。
- **量词**表达范围:
- **全称量词**$\forall$):"对于所有。" $\forall x \, P(x)$ 表示"$P(x)$对论域中的每一个$x$成立。"
- **存在量词**$\exists$):"存在。" $\exists x \, P(x)$ 表示"至少存在一个$x$使得$P(x)$为真。"
- 否定量词会翻转它们:$\neg(\forall x \, P(x)) \equiv \exists x \, \neg P(x)$。"不是所有人都通过了"意味着"有人没通过。"而 $\neg(\exists x \, P(x)) \equiv \forall x \, \neg P(x)$。"没有完美的算法"意味着"每个算法都有缺陷。"
- 嵌套量词表达复杂关系。$\forall x \, \exists y \, (y > x)$ 表示"对于每个数,都有一个更大的数"(对整数成立)。顺序很重要:$\exists y \, \forall x \, (y > x)$ 表示"存在一个比所有其他数都大的数"(对整数不成立)。
- 谓词逻辑是形式化规约的语言。当我们说一个算法是"正确"的,意味着 $\forall \text{输入} \, x, \, \text{输出}(x) = \text{期望输出}(x)$。当我们说它"终止",意味着 $\forall x \, \exists t \, \text{终止}(x, t)$。
## 证明技巧
- **证明**是确立一个陈述真理性、毫无疑义的逻辑论证。与经验证据(仅展示在某些测试案例下有效)不同,证明保证在所有情况下成立。这是计算机科学中正确性的标准。
- **直接证明**:假设前提,通过逻辑步骤推导出结论。要证明"如果$n$是偶数,那么$n^2$是偶数":假设$n = 2k$对于某个整数$k$,则$n^2 = 4k^2 = 2(2k^2)$,这是偶数。
- **反证法**:假设该陈述为假,推导出矛盾。要证明$\sqrt{2}$是无理数:假设$\sqrt{2} = a/b$(已约简)。那么$2 = a^2/b^2$,所以$a^2 = 2b^2$,意味着$a^2$是偶数,所以$a$是偶数,设$a = 2c$。那么$4c^2 = 2b^2$,所以$b^2 = 2c^2$,意味着$b$也是偶数。但我们已经假设$a/b$是约简形式——矛盾。
- **归纳证明**:通过证明以下两点来证明一个陈述对所有自然数成立:(1)**基础情形**成立(通常$n = 0$或$n = 1$),和(2**归纳步骤**:如果陈述对$n = k$成立(归纳假设),那么它对$n = k + 1$也成立。
- 例如,证明 $\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$
- 基础情形:$n = 1$$1 = \frac{1 \cdot 2}{2} = 1$。成立。
- 归纳步骤:假设 $\sum_{i=1}^{k} i = \frac{k(k+1)}{2}$。那么 $\sum_{i=1}^{k+1} i = \frac{k(k+1)}{2} + (k+1) = \frac{k(k+1) + 2(k+1)}{2} = \frac{(k+1)(k+2)}{2}$。这正是$n = k+1$时的公式。证明完成。
- 归纳法是证明递归算法和数据结构性质的主力工具。每个递归算法都暗含一个归纳正确性证明:基础情形是终止条件,归纳步骤是递归调用。
- **强归纳法**假设该陈述对所有不大于$k$的值都成立(不仅仅是$k$),然后证明它对$k + 1$成立。当递归依赖于多个之前的值时,这很有用。
- **鸽巢原理**:如果把$n+1$个物体放入$n$个盒子中,至少有一个盒子包含两个物体。简单但出奇地强大。它证明了在任何13个人中,至少有两个人出生月份相同。在网络中,它证明了当项目数超过桶数时,哈希冲突是不可避免的。
## 集合
- **集合**是不同元素的无序收集。集合是数学中最原始的数据结构,支撑着从类型系统到数据库查询的一切。
- **集合运算**(联系第5章,我们在那里用这些进行概率计算):
- **并集** $A \cup B$:在$A$或$B$或两者中的元素。
- **交集** $A \cap B$:同时在$A$和$B$中的元素。
- **补集** $\bar{A}$:不在$A$中的元素(相对于一个全集)。
- **差集** $A \setminus B$:在$A$中但不在$B$中的元素。
- **笛卡尔积** $A \times B$:所有有序对$(a, b)$,其中$a \in A, b \in B$。
- **幂集** $\mathcal{P}(A)$ 是$A$的所有子集构成的集合。如果 $|A| = n$,那么 $|\mathcal{P}(A)| = 2^n$。对于 $A = \{1, 2\}$$\mathcal{P}(A) = \{\emptyset, \{1\}, \{2\}, \{1, 2\}\}$。
- **基数**衡量集合大小。有限集具有整数基数。无限集有不同的大小:自然数$\mathbb{N}$和有理数$\mathbb{Q}$是**可数无穷**(可以列举),而实数$\mathbb{R}$是**不可数无穷**(无法列举,由康托尔的对角线论证证明)。这种区别在可计算性理论中很重要:存在不可数多个函数,但只有可数多个程序,因此大多数函数是不可计算的。
## 关系
- 集合$A$上的**关系**$R$是$A \times A$的一个子集:指定哪些元素相关联的有序对集合。例如,整数上的$\leq$是集合 $\{(a, b) : a \leq b\}$。
- 关系的重要性质:
- **自反性**:每个元素与自身相关。对所有$a$有$a R a$。例:$\leq$(每个数$\leq$自身)。
- **对称性**:如果$a R b$则$b R a$。例:"是……的兄弟姐妹。"
- **反对称性**:如果$a R b$且$b R a$则$a = b$。例:$\leq$。
- **传递性**:如果$a R b$且$b R c$则$a R c$。例:$<$、$\leq$、"是……的祖先。"
- **等价关系**是自反、对称且传递的。它将集合划分为**等价类**,其中同一类中的所有元素彼此相关,但与不同类中的元素无关。模运算是一个等价关系:$a \equiv b \pmod{n}$ 将整数划分为$n$个类。编程语言中的类型等价是一个等价关系。
- **偏序**是自反、反对称且传递的。它定义了一个"小于等于"结构,可能会使某些元素不可比较。文件系统目录构成一个偏序(父-子),但同级目录是不可比较的。**全序**是每一对元素都可比较的偏序(如整数上的$\leq$)。
- 偏序在并发中至关重要:事件上的"先于发生"关系是一个偏序。不由先于发生关系排序的事件是并发的,可能以任意相对顺序执行。
## 函数
- **函数** $f: A \to B$ 将$A$(定义域)中的每个元素映射到$B$(陪域)中的恰好一个元素。函数是确定性计算的数学模型:给定一个输入,恰好有一个输出。
- **单射**(一对一):不同的输入总是产生不同的输出。$f(a) = f(b) \implies a = b$。无损压缩是单射的:不同的输入必须压缩成不同的输出(否则无法唯一解压)。
- **满射**(到上):$B$中的每个元素都被$A$中的某个元素命中。值域等于陪域。将字符串映射到256位哈希的哈希函数,如果字符串数少于可能的哈希数,则不是满射。
- **双射**:既是单射又是满射。$A$和$B$之间的一一对应。双射具有逆函数。加密必须是双射的:每个明文映射到唯一的密文,而解密函数就是逆函数。
- **复合** $(g \circ f)(x) = g(f(x))$:先应用$f$,再应用$g$。函数复合是可结合的(第2章:就像矩阵乘法是可结合的一样)。软件中的管道就是函数复合:数据流经一系列变换。
## 图论基础
- 我们在第12章(图神经网络)中广泛介绍了图,包括邻接矩阵、图类型、拉普拉斯矩阵和谱理论。这里我们专注于与CS相关的**算法**和**结构**性质。
- **树**是没有环的连通图。等价地,它有$n$个节点和$n-1$条边。树是文件系统、XML/HTML文档、决策过程和递归分解的结构。**有根树**有一个指定的根节点;每个其他节点恰好有一个父节点。
- 图$G$的**生成树**是包含$G$所有节点并使用其边子集的一棵树。**最小生成树(MST)**最小化总边权。Kruskal算法(对边排序,贪心地添加不形成环的最轻边)和Prim算法(从起始节点开始扩展树,总是添加连接到新节点的最轻边)都能在$O(|E| \log |V|)$内找到MST。
- **平面性**:如果一个图可以画在平面上而边不相交,则是平面图。根据**欧拉公式**,对于连通平面图:$|V| - |E| + |F| = 2$,其中$|F|$是面的数量(区域,包括外部面)。这意味着平面图的$|E| \leq 3|V| - 6$,因此平面图是稀疏的。电路板布线和地图着色利用了平面性。
- **图着色**为节点分配颜色,使得没有两个相邻节点共享相同的颜色。所需的最小颜色数是**色数** $\chi(G)$。**四色定理**指出任何平面图的 $\chi(G) \leq 4$。在CS中,图着色模拟寄存器分配(将变量分配到CPU寄存器,使得同时活跃的变量获得不同的寄存器)和调度(将任务分配到时间槽,使得冲突的任务不重叠)。
- **欧拉路径**恰好访问每条边一次。当且仅当图中恰好有0个或2个奇数度节点时,欧拉路径存在。**哈密顿路径**恰好访问每个节点一次。确定哈密顿路径是否存在是NP完全的——这是CS中的经典难题之一。这种对比(欧拉:多项式,哈密顿:NP完全)说明了听起来相似的问题可能具有截然不同的计算复杂度。
## 递推关系
- **递推关系**定义一个序列,其中每一项依赖于前面的项。它们自然地从递归算法中产生。
- 最简单的例子:$T(n) = T(n-1) + 1$,其中 $T(0) = 0$。展开:$T(n) = T(n-1) + 1 = T(n-2) + 2 = \cdots = n$。这是$O(n)$,即简单循环的时间复杂度。
- **归并排序**给出 $T(n) = 2T(n/2) + O(n)$:将数组分成两半(两个大小为$n/2$的子问题),递归排序每一半,然后合并($O(n)$工作)。解为 $T(n) = O(n \log n)$。
- **主定理**求解形式为 $T(n) = aT(n/b) + O(n^d)$ 的递推式:
- 如果 $d > \log_b a$$T(n) = O(n^d)$(每层的工作占主导)
- 如果 $d = \log_b a$$T(n) = O(n^d \log n)$(工作在各层间平衡)
- 如果 $d < \log_b a$$T(n) = O(n^{\log_b a})$(子问题的数量占主导)
- 对于归并排序:$a = 2, b = 2, d = 1$。由于 $d = \log_2 2 = 1$,我们处于平衡情况:$T(n) = O(n \log n)$。
- **斐波那契递推** $F(n) = F(n-1) + F(n-2)$,其中 $F(0) = 0, F(1) = 1$,封闭形式解为 $F(n) = \frac{\phi^n - \psi^n}{\sqrt{5}}$,其中 $\phi = \frac{1+\sqrt{5}}{2}$(黄金比例)且 $\psi = \frac{1-\sqrt{5}}{2}$。这表明斐波那契数列以 $O(\phi^n)$ 指数增长,这就是为什么朴素递归斐波那契指数级慢。
- **组合数学**(排列、组合、二项式定理和容斥原理)在第5章(概率)中介绍。这些计数技术对算法分析至关重要(有多少种可能的输入?需要多少次比较?),但我们在此不再重复。
## 可计算性
- 并非所有事情都能被计算。这是整个数学中最深刻的结论之一,它设定了计算机能力的基本极限。
- **图灵机**是计算的抽象模型:一条无限长的单元格磁带(每个单元格包含一个符号),一个读写头,以及一组带转移规则的有限状态。尽管简单,图灵机可以计算任何实际计算机能计算的任何东西。这就是**邱奇-图灵论题**:任何有效可计算的函数都可以由图灵机计算。
- 每种编程语言(Python、C、Haskell)都是**图灵完备**的:它可以模拟图灵机,从而计算任何可计算的东西。语言之间的区别在于便利性、速度和安全性,而不在于它们根本上能计算什么。
- **停机问题**询问:给定一个程序和一个输入,该程序最终会停止,还是永远运行?图灵(1936)证明不存在能普遍解决这个问题的算法。证明采用反证法:假设存在一个停机检测器 $H(P, x)$。构造一个程序 $D$,它运行 $H(D, D)$ 并做与 $H$ 所说的相反的事。如果 $H$ 说 $D$ 停机,$D$ 就永远循环。如果 $H$ 说 $D$ 循环,$D$ 就停机。矛盾。
- 这不是当前技术的局限;这是一个数学上的不可能性。无论多少计算、多少聪明才智、或多少人工智能,都无法普遍解决停机问题。它是哥德尔不完备定理在计算机科学中的类比。
- 实际后果:你无法编写一个完美的死锁检测器、一个完美的病毒扫描器或一个完美的优化编译器。每一个都需要通用地解决停机问题(或一个等价的不判定问题)。实际工具使用启发式方法和近似方法,在常见情况下有效,但不能保证对所有输入都正确。
- 如果一个问题存在一个总是能给出正确是/否答案并终止的算法,则它是**可判定的**。如果不存在这样的算法,则是**不可判定的**。停机问题是不可判定的。素数测试是可判定的。大多数编程语言中的类型检查是可判定的(通过设计)。
## 复杂度理论
- 即使在可计算的问题中,有些也远比其他的难。**复杂度理论**根据解决问题所需的资源(时间、空间)随输入增长而分类问题。
![P、NP和NP完全:P包含在NP中,NP完全位于边界处,P是否等于NP是核心开放问题](../images/p_np_complexity.svg)
- **P**(多项式时间):能在 $O(n^k)$ 时间内解决的问题,$k$为某个常数。排序($O(n \log n)$)、最短路径($O(|V|^2)$)、矩阵乘法($O(n^3)$)。这些被认为是"高效"或"可处理的。"
- **NP**(非确定性多项式时间):一个拟议的解答能在多项式时间内**验证**的问题,即使**找到**解答可能需要指数时间。例如,给定一个声称的哈密顿路径,你可以通过检查每条边在 $O(n)$ 时间内验证它。但找到一条可能需尝试指数多个可能性。
- P中的每个问题也在NP中(如果你能快速解决它,你当然能快速验证一个解答)。核心问题是 $P = NP$ 是否成立:每个能快速验证解答的问题是否也能快速求解?这是计算机科学中最重要的开放问题,获得克莱数学研究所100万美元的千禧年大奖。
- 大多数专家相信 $P \neq NP$,意味着有些问题本质上比验证更难解决。如果 $P = NP$,密码学将崩溃(破解加密属于NP),而优化、调度和药物设计将变得异常简单。
- **NP完全**问题是NP中最难的问题。一个问题如果是NP完全的,则:(1)它在NP中,且(2)所有其他NP问题可以在多项式时间内**归约**到它。如果你能高效解决任何一个NP完全问题,你就能解决所有NP完全问题(从而 $P = NP$)。
- **归约**将一个问题转换为另一个问题。如果问题A归约到问题B,那么B至少和A一样难。Cook(1971)证明了**SAT**(布尔可满足性:给定一个逻辑公式,是否存在使公式为真的变量赋值?)是NP完全的。Karp(1972)通过将SAT归约到每个问题,证明了其他21个经典问题是NP完全的。
- 著名的NP完全问题:
- **旅行商问题(TSP)**:找到访问所有城市恰好一次的最短路线。
- **图着色**:用$k$种颜色为节点着色,使得没有相邻节点共享同一颜色($k \geq 3$)。
- **子集和问题**:给定一组整数,是否存在一个子集其和等于目标值?
- **布尔可满足性(SAT)**:是否存在使逻辑公式为真的真值赋值?
- **哈密顿路径**(上文图论中提到的)。
- 当你在实践中遇到NP完全问题时,你不会对大规模输入精确求解。相反,你使用:**近似算法**(找到保证在最优解一定倍数范围内的解)、**启发式方法**(贪心、局部搜索、模拟退火)或**特例求解器**(许多NP完全问题对受限输入很容易)。例如,现代SAT求解器尽管在最坏情况下是指数复杂度,但通过利用实际实例中的结构,通常能解决拥有数百万变量的实例。
- **NP困难**问题至少和NP完全问题一样难,但可能不在NP中(它们的解甚至可能不能在多项式时间内验证)。NP完全问题的优化版本通常是NP困难的:"找到最短TSP路线"是NP困难的,而"是否存在一条长度小于$k$的TSP路线?"是NP完全的。
## 编程任务(使用CoLab或笔记本)
1. 构建一个真值表生成器。给定一个逻辑表达式,枚举所有输入组合并计算结果。
```python
import itertools
def truth_table(n_vars, expr_fn):
"""为一个n_vars个变量的布尔函数生成真值表。"""
headers = [f"p{i}" for i in range(n_vars)]
print(" | ".join(headers + ["result"]))
print("-" * (len(headers) * 4 + 10))
for vals in itertools.product([False, True], repeat=n_vars):
result = expr_fn(*vals)
row = [str(v)[0] for v in vals] + [str(result)[0]]
print(" | ".join(f"{r:>2}" for r in row))
# 德摩根定律:NOT(p AND q) == (NOT p) OR (NOT q)
print("德摩根定律验证:")
truth_table(2, lambda p, q: (not (p and q)) == ((not p) or (not q)))
```
2. 通过归纳法证明求和公式——对多个值进行数值验证,然后实现封闭形式解。
```python
import jax.numpy as jnp
# 验证求和公式:sum(1..n) = n(n+1)/2
for n in [1, 5, 10, 100, 1000, 10000]:
brute = sum(range(1, n + 1))
formula = n * (n + 1) // 2
print(f"n={n:5d} sum={brute:>10d} formula={formula:>10d} match={brute == formula}")
```
3. 使用主定理求解归并排序递推关系,并通过计数操作进行经验验证。
```python
import jax.numpy as jnp
def merge_sort_ops(n):
"""统计归并排序中的比较次数(递推:T(n) = 2T(n/2) + n)。"""
if n <= 1:
return 0
half = n // 2
return merge_sort_ops(half) + merge_sort_ops(n - half) + n
for n in [8, 64, 512, 4096, 32768]:
ops = merge_sort_ops(n)
predicted = n * jnp.log2(n)
ratio = ops / predicted
print(f"n={n:5d} ops={ops:>10d} n log n={int(predicted):>10d} ratio={ratio:.3f}")
```