ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

离散数学五大基本结构:集合、函数、序列、和式与矩阵的编程实践

离散数学五大基本结构:集合、函数、序列、和式与矩阵的编程实践 学离散数学最怕什么不是符号多也不是公式抽象而是刚翻开教材就被“集合、函数、序列、和式和矩阵”这五个基本结构给整懵了。名字都眼熟集合高中就学过函数更是老朋友但大学课程里的这套说法和以前学的内容总感觉对不上号。再加上教材一上来就铺开一大堆记号、定义、性质如果没人帮你把这五块内容串成一条线很容易学着学着就变成“背定义抄笔记考试全靠考前突击”。这篇东西我不想按教材的章节顺序走我打算换一个更接地气的角度把集合、函数、序列、和式、矩阵当成一套“描述计算问题的语言”来讲。你看完以后再回去翻Rosen那本大厚书、或者屈婉玲老师的教材会明显感觉那些定义不是孤立的而是有关联的。这篇文章适合正在学离散数学的学生也适合想补数学基础的程序员我会从概念讲到代码尽量让你今天看完明天就能用起来。1. 整体设计与思路拆解这五个结构到底在讲什么1.1 用“学生信息管理系统”把五个结构串起来我讲课和写笔记时最喜欢用一个例子就是学生信息管理系统。这个例子特别朴素但能把五个结构串成一条完整的业务线集合全校学生的名单是一个集合选了Python课的同学是一个集合选了离散数学的同学是另一个集合。那么“两门课都选的人”是什么“只选了一门的人”是什么这就是集合的交集、并集、差集运算。函数每个学生有一个学号学号到姓名是一个映射一个学号对应一个唯一姓名这就是函数。函数描述的是“输入到输出”的对应规则。序列把某个班级的同学按成绩从高到低排出来得到一个有序的名单这就是序列。序列的核心是“位置”和“顺序”同样一批人顺序不同就是不同的序列。和式想求全班平均分就得把所有人的成绩加起来这里就是和式在起作用。和式解决的是“一堆数加起来怎么规范和高效地表示”的问题。矩阵把每个学生每门课的成绩排成一张二维表行是学生列是课程这张表就是矩阵。矩阵特别适合批量处理多维数据。你会发现这五个结构不是五个互不相干的数学玩具它们分别对应着“有哪些对象、对象之间怎么对应、对象怎么排序、总量怎么计算、多维数据怎么组织”。学离散数学的时候只要脑子里始终带着这个例子概念就不容易散架。1.2 为什么计算机领域离不开这五种结构很多人问我学编程会写C和Java就够了为什么还要学集合、函数、矩阵这些东西其实你已经在用了只是没往数学上想。集合对应数据库里的表操作你写SQL时用UNION、INTERSECT本质就是集合的并和交你在Python里用set去重就是在构建一个集合。函数就更不用说编程里的函数、方法、接口核心思想就是“接受输入给出唯一输出”离散数学里的函数对“唯一性”的要求其实就是设计API时的接口规范。序列对应数组、链表、栈、队列算法里的“最长上升子序列”“最大子序列和”问题都是基于序列的计算。和式是算法复杂度分析的日常工具你算一个双重循环的时间复杂度就是在计算一个和式。矩阵就更直接了图形图像里的缩放、旋转、平移机器学习里的权重矩阵都是矩阵运算的舞台。我用一张表把数学概念和编程概念对应起来这样你再看教材时会更有感觉数学概念编程概念典型应用集合set、数据库表、去重操作查重、SQL的UNION/INTERSECT函数函数、接口、映射API设计、lambda表达式序列数组、链表、列表、字符串排序、搜索、动态规划和式循环累加、reduce操作复杂度计算、统计求和矩阵二维数组、NumPy数组图像变换、神经网络、图算法1.3 学习路径与课程主线建议我观察过很多同学离散数学学得不好的九成是学习方法出了问题。他们喜欢从第一页开始逐字逐句读碰到一个定义就停下来背结果背了十页就累了前面的又忘了。我的建议是不要按教材顺序硬啃而是先建立“主线”再补“分支”。主线就是这五个基本结构之间的逻辑关系先有集合因为集合是最底层的“对象容器”然后在集合之上定义函数因为函数是两个集合之间的对应关系接着把函数作用在“有序排列”上得到序列把多个数加起来得到和式把多个维度组合起来得到矩阵。这条主线捋顺了所有零碎定义都能挂上去。教材方面我比较推荐两本。一本是Rosen的《离散数学及其应用》例子多、应用性强英文原版读起来有困难的话可以配合中译本另一本是屈婉玲老师的《离散数学》国内教材里写得很清晰习题质量高。但我想强调不要只囤书不看更不要只看PDF不手写。离散数学是一个“动手学科”必须边看边做例题概念才能变成你自己的。2. 核心细节解析与实操要点2.1 集合从“属于”到“运算”的完整版图集合概念本身不复杂就是一个“把研究对象装在一起”的容器。关键要分清两个记号元素和集合的关系用“属于”$a \in A$集合和集合的关系用“包含于”${a} \subseteq A$。考试里最常见的低级错误就是把“属于”和“包含于”搞混。判断技巧很简单左边是单个元素就看属于左边是带花括号的集合就看包含。集合有三种常见表示方法列举法、描述法、文氏图。列举法适合有限集合比如$A{1,2,3}$描述法适合元素规律明显的集合比如$A{x \mid x是偶数, x0}$文氏图适合理解并、交、差、补这些运算关系。集合的核心运算不多我用一张表把所有运算的记法和Python对应操作列出来方便你对照记忆运算数学记号含义Python写法并集$A \cup B$属于A或属于B的所有元素A | B交集$A \cap B$同时属于A和B的元素A B差集$A - B$属于A但不属于B的元素A - B补集$\overline{A}$全集中不属于A的元素无直接运算符需自己定义对称差$A \oplus B$属于A和B中恰好一个集合的元素A ^ B此外集合有几个“看起来简单但容易被忽略”的性质空集是任何集合的子集任何集合都是它自身的子集。如果一个集合有$n$个元素那么它的幂集$P(A)$有$2^n$个元素。这个结论在做“3个元素的集合有多少拓扑”这类题时会用到本质就是“每个元素选或是不选”共有$2^n$种组合。集合运算还满足交换律、结合律、分配律和德摩根律。德摩根律是$\overline{A \cup B} \overline{A} \cap \overline{B}$$\overline{A \cap B} \overline{A} \cup \overline{B}$。理解它有个生活化类比你说“我没有苹果也没有香蕉”等于“我没有苹果而且也没有香蕉”你说“我不是又高又富有”等价于“我不高或者我不富有”。这个类比比死记公式靠谱得多。2.2 函数一对一的“接口约定”函数在离散数学里的定义是从集合A到集合B的一个映射使得A中的每个元素都对应B中唯一的一个元素。写为$f: A \to B$。你注意“唯一”这个词这正是函数和一般关系的区别。关系允许一个输入对应多个输出函数不允许。判断函数性质时有三个重要概念我建议用“查户口”的方式理解单射对应关系“不会撞车”。不同的$x$一定对应不同的$f(x)$。用程序员的说法就是主键不可以重复。满射B中的每个元素“都有对象”。即每个可能的输出都至少被某个输入映到一次。用程序员的说法就是值域等于陪域。双射既单射又满射一对一且全覆盖。双射的存在意味着两个集合的大小“一样多”。遇到具体函数时分类判定可以用这张表函数性质判定方法示例单射如果$f(x_1)f(x_2)$能推出$x_1x_2$$f(x)2x$是单射$f(x)x^2$不是满射值域恰好等于陪域B中没有落空的元素从${1,2,3}$到${a,b}$的满射例子很多双射既单射又满射$f(x)x1$从整数集到整数集是双射函数复合也是一个高频考点。$f \circ g$表示先做$g$再做$f$即$(f \circ g)(x)f(g(x))$。这里特别容易搞反顺序我每次做题都会先在心里读一遍“这个符号右边的先执行”。如果$f$是双射那么它有反函数$f^{-1}$反函数的意思就是把箭头全反过来还是一一对应。我在实际编码里还有个体会离散数学里的函数很像编程里的纯函数——同样的输入永远给出同样的输出没有副作用。理解了这一点函数式编程里的“不可变性”和“引用透明”就不难理解了。2.3 序列与和式排序世界里的“循环表达式”序列就是按照一定顺序排列的元素写为$a_1, a_2, a_3, \ldots, a_n$。序列与集合最大的区别是集合里的元素没有顺序、不能重复序列里的元素有序、可以重复。简单说${1,2,3}$和${3,2,1}$是同一个集合但$(1,2,3)$和$(3,2,1)$是两个不同的序列。序列有两种表示方式显式公式和递推公式。显式公式直接给出$a_n$关于$n$的表达式比如$a_n3n1$递推公式给出前项和后项的关系比如$a_{n1}a_n2$需要给定初始值。大名鼎鼎的斐波那契数列就是递推公式的典型$F_11,F_21,F_nF_{n-1}F_{n-2}$。递推在算法里就是“状态转移方程”动态规划的本质就是在序列上做递推。等差序列和等比序列的前$n$项和公式是考试和复杂度计算的常客。等差数列求和是$\frac{n(a_1a_n)}{2}$等比数列求和是$\frac{a_1(1-q^n)}{1-q}$当$q \neq 1$时。我建议你把这两个公式当成“肌肉记忆”尤其是等差数列公式以后分析算法复杂度时经常会碰到。和式的表示也很简单$\sum_{i1}^{n} a_i$意思是把$a_i$从$i1$一直加到$in$。它最容易被忽视的地方是求和上下标的处理。做题时一定要先看清是从几加到几比如$\sum_{i0}^{n} 2^i$和$\sum_{i1}^{n} 2^i$结果差一个$1$。常用求和公式我建议背这几个$\sum_{i1}^{n} i \frac{n(n1)}{2}$$\sum_{i1}^{n} i^2 \frac{n(n1)(2n1)}{6}$$\sum_{i0}^{n} 2^i 2^{n1}-1$。从编程视角看序列就是一个数组或链表和式就是一个循环累加。你每次写sum a[i]本质上就是在计算一个和式你每次用for i in range(1, n1)就已经在枚举序列了。2.4 矩阵二维数据的“批量处理说明书”矩阵可以看成是一个矩形的数表几行几列写成$m \times n$。比如$3 \times 3$矩阵就是三行三列。矩阵里的数称为元素用双下标表示比如$a_{ij}$表示第$i$行第$j$列的元素。编程里数组下标是从0开始而数学里矩阵下标从1开始做题和写代码时要特别留心这个差异。矩阵的基本运算有三种加法、数量乘法、矩阵乘法。矩阵加法要求两个矩阵维度完全相同就是对应位置相加。矩阵乘法稍微特殊$A$是$m \times n$$B$是$n \times p$乘积$CA \times B$是$m \times p$其中$c_{ij}\sum_{k1}^{n} a_{ik}b_{kj}$。可以理解为C的第$i$行第$j$列元素等于A的第$i$行和B的第$j$列对应元素相乘再求和。矩阵乘法有个非常经典的坑不满足交换律。也就是$A \times B$一般不等于$B \times A$。很多人初学的时候习惯性按普通乘法套用做题一下子就错了。记住一句话矩阵乘法是“行列点积”行乘过去列加过来方向不能反过来。除了基本运算特殊矩阵也经常考。零矩阵是所有元素都是0单位矩阵$I$是主对角线为1、其余为0的方阵它相当于矩阵世界里的“1”任何矩阵乘单位矩阵都等于它自己对角矩阵是主对角线以外全是0的矩阵。对称矩阵满足$A^TA$即转置后等于自身。矩阵的行列式、逆矩阵、特征值分解是进阶内容。特征值分解的本质是把一个矩阵分解成“特征向量矩阵 × 特征值对角矩阵 × 特征向量逆矩阵”的形式。你不需要在基本离散结构这一章就往深里钻但可以先建立一个直觉特征值告诉我一个矩阵在各个方向上的“伸缩比例”这个思想在机器学习的主成分分析、图像压缩里都会用到。3. 实操过程与核心环节实现用代码验证数学结论3.1 用Python实现集合运算与可视化纸上谈兵不够我强烈建议你打开Python环境亲手把集合运算跑一遍。Python里的set类型天然支持数学集合运算代码非常直观A {1, 2, 3, 4} B {3, 4, 5, 6} print(A | B) # 并集{1, 2, 3, 4, 5, 6} print(A B) # 交集{3, 4} print(A - B) # 差集{1, 2} print(B - A) # 差集{5, 6} print(A ^ B) # 对称差{1, 2, 5, 6}再实现一个求幂集的函数用来验证“$n$个元素的集合幂集有$2^n$个元素”这个结论from itertools import combinations def power_set(s): s list(s) result [] for r in range(len(s) 1): for comb in combinations(s, r): result.append(set(comb)) return result A {1, 2, 3} ps power_set(A) print(len(ps)) # 输出 8 print(ps)如果你想把文氏图画出来可以装matplotlib-venn这个库。画图的意义不是炫技而是让你直观看到“交集是两个圈重叠的部分”“差集是一个圈去掉重叠部分剩下的区域”。我看过不少同学画了几张文氏图之后德摩根律就再也没记错过因为脑子里的图像已经形成了。实际操作中有一个坑必须提醒Python的set元素必须是不可变类型也就是说你不能把列表或字典放进集合里。想放一个“包含多个元素的组合”先转成元组tuple。这个细节点在刷题时经常坑人。3.2 用Python判断函数性质并演示函数复合函数在集合论里的本质是特殊的二元关系。我可以用字典来模拟一个有限函数比如$f:{1,2,3}\to{a,b,c}$定义为{1: a, 2: b, 3: c}。判断单射和满射的代码非常简单def is_injective(f): # 单射不同的输入输出各不相同 return len(set(f.values())) len(f.values()) def is_surjective(f, codomain): # 满射输出集合覆盖整个陪域 return set(f.values()) set(codomain) f {1: a, 2: b, 3: c} print(is_injective(f)) # True print(is_surjective(f, [a, b, c])) # True函数复合的实现也很直观。先做内层函数g再做外层函数fdef compose(f, g): # 返回 f ∘ g表示先 g 后 f result {} for x, gx in g.items(): result[x] f[gx] return result f {a: 1, b: 2, c: 3} g {1: a, 2: b, 3: c} h compose(f, g) print(h) # {1: 1, 2: 2, 3: 3}这就是恒等映射我建议你把代码跑一遍然后对比教材里的复合函数定义。你就会发现“$f \circ g$是先执行$g$”这件事在代码里是“先查g的表再拿结果去查f的表”非常直白。3.3 用Python生成序列并计算和式序列和和式的代码实现是理解“循环”和“数学归纳”的绝佳抓手。比如生成一个等差数列并验证前$n$项和公式a1 1 d 3 n 10 seq [a1 (i - 1) * d for i in range(1, n 1)] # 暴力求和 total sum(seq) # 公式求和 formula n * (a1 seq[-1]) // 2 print(seq) # [1, 4, 7, 10, 13, 16, 19, 22, 25, 28] print(total) # 145 print(formula) # 145斐波那契数列用递推来实现也是面试里反复出现的题def fibonacci(n): a, b 1, 1 for _ in range(n - 1): a, b b, a b return a print([fibonacci(i) for i in range(1, 10)]) # [1, 1, 2, 3, 5, 8, 13, 21, 34]再介绍一个实际项目里很有用的技巧如果在线性序列上做累加改用生成器表达式而不是一次性生成列表可以省不少内存total sum(i * i for i in range(1, 1000001)) print(total)这个写法本质上就是和式$\sum_{i1}^{n} i^2$的代码表达。注意这里的i * i for i in range(...)是一个生成器不会一次性把100万个平方数全部存进内存性能比列表推导式好很多。3.4 用NumPy完成矩阵运算与特征值分解矩阵运算在Python里最常用的工具是NumPy。如果你做图像处理、数据分析或者机器学习几乎天天和它打交道。基本操作如下import numpy as np A np.array([[1, 2], [3, 4]]) B np.array([[5, 6], [7, 8]]) print(A B) # 矩阵加法 print(2 * A) # 数量乘法 print(A B) # 矩阵乘法注意用的是 print(A.T) # 转置初学者最容易踩的坑是混淆*和。A * B在NumPy里是对应元素相乘Hadamard积不是数学上的矩阵乘法A B才是线性代数里的矩阵乘法。如果写成A * B系统不报错但结果完全不对这种隐蔽错误最耗调试时间。特征值分解的代码只要一行A np.array([[2, 1], [1, 2]]) eigenvalues, eigenvectors np.linalg.eig(A) print(特征值, eigenvalues) print(特征向量\n, eigenvectors)输出结果里特征值表示矩阵作用在对应特征向量方向上的伸缩比例。这个概念在基本离散结构阶段不需要深究但提前体验一下后面学到矩阵论或机器学习时会有亲切感。NumPy矩阵下标也需要注意它遵循Python从0开始不是数学里的从1开始所以A[0][1]对应矩阵的第1行第2列也就是数学记号里的$a_{12}$。写代码和读教材时下标要来回切换这个转换能力本身就是一种实战能力。4. 常见问题与排查技巧实录4.1 概念易混点速查考前最好过一遍我把这些年批改作业、答疑时最常看到的错误汇总成了一张表。这张表我建议你在期末复习时过一遍基本能覆盖80%的“概念混淆”失分点易混点正解错误理解属于 vs 包含$a \in A$是元素属于集合${a} \subseteq A$是集合包含于集合把两者混用函数 vs 关系每个输入有唯一输出觉得多值也能叫函数单射 vs 满射单射看输出是否重复满射看输出是否全覆盖把单射当成满射矩阵乘法顺序$AB$中A的列数必须等于B的行数且一般$AB \neq BA$按普通乘法交换顺序求和上下标$\sum_{i0}^{n}$和$\sum_{i1}^{n}$结果差第一项直接忽略下标范围集合元素是否重复集合无重复元素把序列的有序且可重复套到集合上还有一个我之前反复讲过的点集合里的“元素”可以是集合本身比如${{1,2},3}$这种情况看清外层花括号就行。做题时不要想当然地“把括号去掉”每一步都要回到定义。4.2 编程实现时的常见报错与心理预期如果你跟着上面的代码实操大概率会遇到几个小问题我提前说一下排查方法。第一个是TypeError: unhashable type: list。这个报错出现在你试图把列表放入集合或者把列表作为字典的键时。解决方案是先把列表转成元组比如set([1, 2])这种写法没问题但set([[1,2]])就会报错要写成set([(1,2)])。第二个是NumPy中矩阵乘法结果不符合预期。检查一下你有没有把写成*。如果两个矩阵形状是(2,2)A * B和A B都能运行但意义完全不同。这提醒我们代码能跑不代表逻辑正确一定要打印中间结果验证。第三个是复合函数时KeyError。比如前面compose的例子如果f的键域和g的值域不一致就会找不到对应的键。这其实对应数学里的一个前提条件$g$的值域必须是$f$的定义域的子集。数学条件在代码里就表现为“查表不能查空”两者是一致的。遇到这类报错你要检查的不是代码语法而是两个映射的定义域和值域是否匹配。还有一个小坑是浅拷贝问题。用list.copy()复制二维列表时内层列表仍然是同一个对象修改一个会影响另一个。矩阵操作用NumPy可以避免很多这种问题但如果你坚持用纯Python写二维列表赋值时请用深拷贝copy.deepcopy()。4.3 自学资源与工具推荐踩坑后的个人推荐最后聊点个人向的资源推荐。教材方面我前面提的Rosen和屈婉玲两本都是经典但如果你觉得Rosen的书太厚、太容易劝退可以先用屈婉玲的教材打底再回头啃Rosen里的应用例子。刷题时不要只做选择填空一定要动手写解答题尤其要写“证明两个集合相等”这类题它是训练逻辑推导最好的方式。在线工具方面函数图像绘制可以用Desmos或GeoGebra输入公式立刻出图像对理解函数性质帮助很大。矩阵计算可以搜在线矩阵计算器验证自己手算的乘法结果省去检查算错的烦恼。但工具再好也代替不了手推。我见过的学生里凡是课上划水、只靠工具对答案的期末考试基本都吃亏。笔记方法上我建议做“概念-例子-代码”三段式笔记。每个概念右边配一个数学例子下面再配一段Python代码。这样复习的时候数学概念和程序实现互相印证比单看教材效率高很多。在互联网上你也能找到不少别人整理好的“离散数学笔记”但我的体会是别人的笔记只能用来查缺补漏自己动手写一遍才是真正过脑子。最后再分享一个小技巧。学离散数学尤其是基本离散结构这一章真正管用的办法是“双向翻译”看到一个数学公式想一想它对应的Python代码长什么样看到一段循环代码想一想它对应的数学公式是什么。比如sum(a[i] for i in range(1, n1))和$\sum_{i1}^{n}a_i$练上几道题你就再也不会觉得数学符号和代码是两套语言了。这套“翻译思维”不只是为了应付期末考它会在你以后读论文、看算法资料、设计系统的时候持续给你回报。
返回列表