18.06 第 19 讲:行列式公式与代数余子式
- 118.06 第 1 讲:方程组的行图与列图
- 218.06 第 2 讲:用矩阵做消元
- 318.06 第 3 讲:矩阵乘法的四种看法与逆矩阵
- 418.06 第 4 讲:分解成 A = LU
- 518.06 第 5 讲:转置、置换与向量空间
- 618.06 第 6 讲:列空间与零空间
- 718.06 第 7 讲:解 Ax = 0,主变量与特解
- 818.06 第 8 讲:解 Ax = b,行最简形 R
- 918.06 第 9 讲:线性无关、基与维数
- 1018.06 第 10 讲:四个基本子空间
- 1118.06 第 11 讲:矩阵空间、秩 1 矩阵与小世界图
- 1218.06 第 12 讲:图、网络与关联矩阵
- 1318.06 第 13 讲:第一次测验复习
- 1418.06 第 14 讲:正交向量与正交子空间
- 1518.06 第 15 讲:投影到子空间
- 1618.06 第 16 讲:投影矩阵与最小二乘
- 1718.06 第 17 讲:正交矩阵与 Gram-Schmidt
- 1818.06 第 18 讲:行列式的性质
- 1918.06 第 19 讲:行列式公式与代数余子式本文
- 2018.06 第 20 讲:Cramer 法则、逆矩阵与体积
- 2118.06 第 21 讲:特征值与特征向量
- 2218.06 第 22 讲:对角化与矩阵的幂
- 2318.06 第 23 讲:微分方程与 exp(At)
- 2418.06 第 24 讲:Markov 矩阵与 Fourier 级数
- 2518.06 第 24b 讲:第二次测验复习
- 2618.06 第 25 讲:对称矩阵与正定性
- 2718.06 第 26 讲:复矩阵与快速 Fourier 变换
- 2818.06 第 27 讲:正定矩阵与极小值
- 2918.06 第 28 讲:相似矩阵与 Jordan 标准形
- 3018.06 第 29 讲:奇异值分解
- 3118.06 第 30 讲:线性变换及其矩阵
- 3218.06 第 31 讲:基变换与图像压缩
- 3318.06 第 32 讲:第三次测验复习
- 3418.06 第 33 讲:左逆、右逆与伪逆
- 3518.06 第 34 讲:期末复习
上一讲 Strang 用三条性质推出了十条性质,这一讲他把同样的三条性质用到底,得到一个能直接从 算出行列式的公式:4 × 4 矩阵有 24 项,6 × 6 有 720 项,每一项都是“每行每列各取一个数”的乘积,再带上正负号。我拿他在黑板上现编的 4 × 4 例子去数,24 项里只有两项不是零,一项 、一项 ,加起来是 0。之后是第二个公式(余子式展开),以及一个三对角矩阵的行列式:它每六阶重复一次,所以 。
视频:Lecture 19: Determinant formulas and cofactors(OCW 页面与字幕) 教材:《Introduction to Linear Algebra》第 6 版 §5.1 3 by 3 Determinants and Cofactors;§5.2 Computing and Using Determinants 前置:第 18 讲:行列式的性质 一句话:行列式等于 项“每行每列各取一个”的带符号乘积(大公式),也等于沿一行的 乘以各自的代数余子式之和,后者把 阶行列式拆成 阶的。
- 开头他先用三条性质把 2 × 2 的 重新推了一遍,只是为了展示方法。这一段值得认真听,后面全是这个套路的重复。
- 3 × 3 的“幸存者”那段是整讲的核心:把每一行拆开,二十七块里只有每行每列各一个的才活下来,他还逐个判断了正负号。我把它写成了
itertools.permutations,本篇第一个代码块里算过。 - 现编的 4 × 4 例子(二十四项里二十二项为零)他自己说“以前没算过”,可以认真看他怎么把整个过程走下来。矩阵他写在黑板上,字幕里没有,我按他描述的路径还原了一个。
- 代数余子式的定义和符号规则(棋盘格)也要看;最后三对角矩阵的例子很短,周期是 6 那里是整讲最有意思的地方,我在最后一个代码块里算过。
把三条性质用到底:大公式
行列式到底由什么构成? 回忆上一讲的三条性质:;交换两行,行列式变号;对每一行单独看,行列式是线性的。Strang 先在 2 × 2 上演示怎么用它们算出一个公式。
做法是把第一行 拆成 ,再把第二行 拆成 ,用性质三(对每一行线性)得到四个行列式:
第一个和第四个有一整列是 0,是奇异矩阵,行列式为 0,他说“它死了”。第二个是对角矩阵,行列式 。第三个要换一次行才变成对角矩阵,行列式是 。四项里两项活下来, 就这么出来了。
重点是方法,不是这个答案。3 × 3 时,第一行拆成三块,第二行再拆成三块,第三行再来一次,一共 块。我数了一下,4 × 4 是 块。这些块里,只要某一列没被任何一行选到,那一列全是零,整块的行列式为 0。
幸存者是谁? 每一行选一个位置,每一列也恰好被选一次的那些块。他的 3 × 3 里有六个幸存者,对应列号 、、、、、 的排列。每一块把选中的数提出来,剩下的是一个只有 0 和 1 的置换矩阵,它的行列式是 ,由把它换回单位矩阵需要的行交换次数的奇偶决定。 要换一次,所以是 ; 要换两次,所以是 。合起来:
正号三项沿着“往下”的方向,负号三项沿着“反方向”,这是他口中去迈阿密和洛杉矶的那两条线。他立刻说这个口诀只对 3 × 3 成立:4 × 4 的反对角线(全是 1 的那条)行列式是 ,因为交换第 1、4 行和第 2、3 行共两次就回到单位矩阵。
推广到 阶,就是他书里的大公式(big formula):
第一行有 种选法,选完占掉一列,第二行剩 种,依此类推,一共 项,一半正一半负。正负号看排列的奇偶。我猜这条公式如果做成棋盘上的游戏,就是在 的棋盘上放 个互不攻击的车,每种放法对应一项(这个类比我只用这一次)。
下面把它实现出来。itertools.permutations 给出所有列号排列,符号用逆序对的个数的奇偶判断(相邻两个数交换一次,逆序对数恰好变 1,所以它的奇偶和“交换次数的奇偶”是同一回事)。脚本先打印 3 × 3 的六个排列和符号,再数反对角线的行列式,数 的项数,最后在一个 6 × 6 的随机矩阵上对比 np.linalg.det:
import itertools
import numpy as np
def perm_sign(perm: tuple[int, ...]) -> int: """数逆序对的个数,奇数个就是奇置换。""" inversions = sum( 1 for i in range(len(perm)) for j in range(i + 1, len(perm)) if perm[i] > perm[j] ) return -1 if inversions % 2 else 1
def big_formula(A: np.ndarray) -> float: """行列式的大公式:每行每列各取一个数,共 n! 项。""" n = A.shape[0] total = 0.0 for perm in itertools.permutations(range(n)): term = perm_sign(perm) for row, col in enumerate(perm): term *= A[row, col] total += term return total
def main() -> None: np.set_printoptions(precision=4, suppress=True)
# 3 x 3 的六项:列号的排列、符号 for perm in itertools.permutations(range(3)): cols = tuple(c + 1 for c in perm) print(cols, "符号", f"{perm_sign(perm):+d}")
# 反对角线:4 x 4 是 +1,3 x 3 是 -1 for n in (2, 3, 4): J = np.fliplr(np.eye(n)) print(n, "阶反对角线的行列式:", round(np.linalg.det(J)))
# 项数:n! for n in (2, 3, 4, 5, 6): count = sum(1 for _ in itertools.permutations(range(n))) print(f"n = {n}:{count} 项")
rng = np.random.default_rng(1806) A = rng.standard_normal((6, 6)) print(f"大公式:{big_formula(A):.6f}") print(f"np.linalg.det:{np.linalg.det(A):.6f}") print("一致:", np.isclose(big_formula(A), np.linalg.det(A)))
if __name__ == "__main__": main()(1, 2, 3) 符号 +1(1, 3, 2) 符号 -1(2, 1, 3) 符号 -1(2, 3, 1) 符号 +1(3, 1, 2) 符号 +1(3, 2, 1) 符号 -12 阶反对角线的行列式: -13 阶反对角线的行列式: -14 阶反对角线的行列式: 1n = 2:2 项n = 3:6 项n = 4:24 项n = 5:120 项n = 6:720 项大公式:-7.439860np.linalg.det:-7.439860一致: True六个排列里三正三负,和他黑板上写的一致;反对角线单位矩阵的行列式是 2 阶 、3 阶 、4 阶 ,确认了“口诀只对 3 × 3 成立”。6 × 6 矩阵用 720 项算出的 和 np.linalg.det 完全对上。
他还指出,大公式可以反推所有性质: 时,只有 这一项(排列是 ,符号为正)不为零,所以 。同样可以看出 ,因为转置只是把“行选列”换成“列选行”。至于 ,他说从这条公式出发证明会弄到“下个星期还在做”,大概就是不会再提了。
一个例子:二十四项里只有两项活着
Strang 现场编了一个 4 × 4 的 0-1 矩阵,说“我不知道它的行列式,数学正在你们眼前第一次被做出来”。字幕里没有写出矩阵,只有他的走法:第一条路取出排列 ,交换两次回到 ,是 ;第二条路是 ,只要交换 1 和 3 一次,是 ;从别的位置出发都走不通。两条路加起来是 0,所以奇异。他接着发现第一行加第三行是全 1 的行,第二行加第四行也是,所以“行一减行二加行三减行四”是零行。
我按这些线索还原了一个满足所有描述的矩阵(我的版本可能没有他的那么有戏剧性):
把 24 个排列都过一遍,看有哪些乘积不为零:
import itertools
import numpy as np
def perm_sign(perm: tuple[int, ...]) -> int: inversions = sum( 1 for i in range(len(perm)) for j in range(i + 1, len(perm)) if perm[i] > perm[j] ) return -1 if inversions % 2 else 1
def main() -> None: np.set_printoptions(precision=4, suppress=True) # 字幕里没有写出矩阵,这是我按他的两条路径还原的 0-1 矩阵 A = np.array( [ [0, 0, 1, 1], [0, 1, 1, 0], [1, 1, 0, 0], [1, 0, 0, 1], ], dtype=float, )
survivors = [] for perm in itertools.permutations(range(4)): term = np.prod([A[row, col] for row, col in enumerate(perm)]) if term != 0: survivors.append((tuple(c + 1 for c in perm), perm_sign(perm))) print("24 项里不为零的:", survivors) total = sum(sign for _, sign in survivors) print("它们的和:", total) print("np.linalg.det:", round(np.linalg.det(A), 10) + 0.0)
# 奇异的另一个证据:行的组合给出零行,列的组合给出零向量 print("行一 - 行二 + 行三 - 行四:", A[0] - A[1] + A[2] - A[3]) print("列一 - 列二 + 列三 - 列四:", A[:, 0] - A[:, 1] + A[:, 2] - A[:, 3]) print("秩:", np.linalg.matrix_rank(A))
if __name__ == "__main__": main()24 项里不为零的: [((3, 2, 1, 4), -1), ((4, 3, 2, 1), 1)]它们的和: 0np.linalg.det: 0.0行一 - 行二 + 行三 - 行四: [0. 0. 0. 0.]列一 - 列二 + 列三 - 列四: [0. 0. 0. 0.]秩: 324 项里恰好两项不为零,列号 带 , 带 ,和他的说法一致。和为 0,np.linalg.det 也给出 0.0,秩是 3。行组合和列组合都能凑出零,所以零空间和左零空间各有一个非零向量。这就是“零多的矩阵可以直接用大公式”的好处:一眼看出来的结构,比消元更快。
代数余子式:把 阶拆成 阶
怎样把大公式拆开? 还是从 3 × 3 开始。把含有 的两项合并:
括号里正好是划掉第 1 行第 1 列之后剩下的 2 × 2 矩阵的行列式。含 的两项是 ,括号里同样是划掉第 1 行第 2 列后的 2 × 2 行列式,但前面多了一个负号。含 的是 。
Strang 把“划掉后剩下的子矩阵行列式”连同这个符号一起叫作 的代数余子式(cofactor):
是划掉第 行第 列后的 矩阵。符号由 的奇偶决定,是一张棋盘格,左上角是加号。不带符号的 叫余子式(minor),他说那个词他不在乎,直接把符号收进去叫代数余子式。沿第一行展开:
沿第 行同理,只是把下标 1 换成 。2 × 2 的例子最小: 的余子式是 , 的余子式是 ,加上符号后是 ,所以 。
他还提到,既然余子式能把 阶拆成 阶,再拆成 阶,一路拆到 1 × 1,那么整门行列式的课完全可以从这个公式开始讲,有人就是这么讲的。他选择先讲性质,原因我猜是性质更容易看出行列式“为什么”这样。
他接着把三种算法的位置交代清楚:主元的乘积把所有繁琐的东西已经折进了消元里,效率最高;大公式把所有繁琐的东西摊开写出来;余子式公式介于两者之间,“容易的东西乘以可怕的东西”,它的用处是把大行列式拆成小行列式。
下面是余子式展开的递归实现。det_cofactor 沿指定的一行展开,每次调用时全局的计数器加一,用来数递归调用次数;我还对 6 × 6 矩阵每一行都展开一次,确认结果相同,并和 np.linalg.det 对照:
import numpy as np
calls = 0
def minor(A: np.ndarray, i: int, j: int) -> np.ndarray: """划掉第 i 行第 j 列。""" return np.delete(np.delete(A, i, axis=0), j, axis=1)
def cofactor(A: np.ndarray, i: int, j: int) -> float: """代数余子式 C_ij = (-1)^(i+j) det(M_ij)。""" return (-1) ** (i + j) * det_cofactor(minor(A, i, j))
def det_cofactor(A: np.ndarray, row: int = 0) -> float: """沿第 row 行做余子式展开。""" global calls calls += 1 n = A.shape[0] if n == 1: return A[0, 0] return sum(A[row, j] * cofactor(A, row, j) for j in range(n))
def main() -> None: global calls np.set_printoptions(precision=4, suppress=True)
# 符号是棋盘格 signs = np.array([[(-1) ** (i + j) for j in range(5)] for i in range(5)]) print(signs)
# 2 x 2:a * d + b * (-c) B = np.array([[3.0, 5.0], [2.0, 7.0]]) print("2 x 2:", det_cofactor(B), ",np.linalg.det:", np.linalg.det(B))
# 沿任意一行展开,结果相同 rng = np.random.default_rng(1806) A = rng.standard_normal((6, 6)) for row in range(6): print(f"沿第 {row + 1} 行:{det_cofactor(A, row):.6f}") print(f"np.linalg.det:{np.linalg.det(A):.6f}")
# 递归调用次数,对比 n! print("n 调用次数") for n in (3, 4, 5, 6, 7, 8): calls = 0 det_cofactor(rng.standard_normal((n, n))) print(f"{n} {calls}")
if __name__ == "__main__": main()[[ 1 -1 1 -1 1] [-1 1 -1 1 -1] [ 1 -1 1 -1 1] [-1 1 -1 1 -1] [ 1 -1 1 -1 1]]2 x 2: 11.0 ,np.linalg.det: 11.000000000000002沿第 1 行:-7.439860沿第 2 行:-7.439860沿第 3 行:-7.439860沿第 4 行:-7.439860沿第 5 行:-7.439860沿第 6 行:-7.439860np.linalg.det:-7.439860n 调用次数3 104 415 2066 12377 86608 69281棋盘格的符号和他说的一样(第二行从减号开始)。沿六行中任意一行展开都得到 ,与 np.linalg.det 一致,也与上一节用大公式算出的同一个矩阵的值一致。
代价是另一回事:3 阶调用 10 次,8 阶调用 69281 次。这个调用次数已经比 大了 1.7 倍,因为余子式展开除了叶子还有中间层。这是稠密矩阵的情形;到 20 阶, 约 ,而消元只需要大约 次乘法(见第 4 讲)。所以在实际计算里算行列式用消元,大公式和余子式的价值在于它们给出了结构:零很多的时候可以一眼读出结论,符号规则也是后面逆矩阵公式的基础。
三对角矩阵:行列式每六阶重复一次
零很多的时候,余子式能做出什么? Strang 的最后一个例子是对角线和两条副对角线全是 1 的 阶矩阵 ,4 阶是
小的先算:,。 的大公式有六项,其中三项非零,、、,和是 ;也可以先用第二行减第三行,再沿新的行展开,得到 。
到 ,沿第一行展开,只有两个非零元素。 的余子式是 的行列式,符号为正。 在 位置,符号为负,划掉第一行第二列后的 3 × 3 矩阵的第一列是 ,对这一列再展开一次(转置规则允许按列展开),剩下的是 ,符号是正。所以 。这个论证对任何 都成立:
他说这些数你大概猜不到,然后一路往下算:1、0、、、0、1、1、……,到这里又开始重复,周期是 6( 是 0, 和 都是 1)。所以 ()。
我把递推写成循环,从 1 阶到 12 阶与 np.linalg.det 逐个对照,再算 61 阶:
import numpy as np
def tridiagonal_ones(n: int) -> np.ndarray: """对角线与两条副对角线全是 1 的 n 阶矩阵。""" return np.eye(n) + np.eye(n, k=1) + np.eye(n, k=-1)
def det_recurrence(n: int) -> int: """D_n = D_{n-1} - D_{n-2},D_0 = 1,D_1 = 1。""" prev, curr = 1, 1 if n == 0: return prev for _ in range(n - 1): prev, curr = curr, curr - prev return curr
def main() -> None: np.set_printoptions(precision=4, suppress=True) print(tridiagonal_ones(4))
print("n 递推 np.linalg.det") for n in range(1, 13): D = round(np.linalg.det(tridiagonal_ones(n))) print(f"{n:<3d} {det_recurrence(n):>3d} {D:>3d}")
n = 61 exact = det_recurrence(n) numeric = np.linalg.det(tridiagonal_ones(n)) print(f"n = {n}:递推 = {exact},np.linalg.det = {numeric:.6f}")
if __name__ == "__main__": main()[[1. 1. 0. 0.] [1. 1. 1. 0.] [0. 1. 1. 1.] [0. 0. 1. 1.]]n 递推 np.linalg.det1 1 12 0 03 -1 -14 -1 -15 0 06 1 17 1 18 0 09 -1 -110 -1 -111 0 012 1 1n = 61:递推 = 1,np.linalg.det = 1.00000012 阶以内每一阶的递推值和数值行列式都一致:1、0、、、0、1,然后 1、0、、、0、1。61 阶的 矩阵让 np.linalg.det 也给出 1.000000,和递推相同。注意 的行列式是 0,说明直接消元的话第二个主元会出问题,需要换行;NumPy 内部是带行交换的 LU,所以不受影响。
这个例子我觉得挺漂亮的:一个看起来只能一阶一阶硬算的行列式,因为余子式的递推,变成了一个周期为 6 的数列。余子式在这里比消元好用,是因为矩阵的零恰好让每次展开只剩两项。
下一讲是行列式的最后一讲,他会用余子式给出逆矩阵的公式,再把 Cramer 法则和体积放进来:Cramer 法则、逆矩阵与体积。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!











