0. 这一节到底在讲什么
前面几节我们一直在用矩阵做消元。这一节要回答一个更基础的问题:矩阵到底要怎么"乘"?
矩阵乘法不是把对应位置的数字相乘(那是另一种运算,叫"哈达玛积",这里不讨论)。矩阵乘法有自己的一套规则,而且这套规则是"设计"出来的——之所以这样定义,是因为这样定义之后,矩阵才能用来表示"先做一次变换,再做一次变换"这种连续的操作。
这一节会给出四种理解矩阵乘法 ABABAB 的方式,它们算出来的结果完全一样,只是看问题的角度不同。理解了这四种角度,后面学线性方程组、子空间、特征值的时候会轻松很多。
1. 能不能乘?——维度匹配规则
设矩阵 AAA 是 mmm 行 nnn 列(写作 m×nm \times nm×n),矩阵 BBB 是 nnn 行 ppp 列(写作 n×pn \times pn×p)。
规则:只有当 AAA 的列数等于 BBB 的行数时,才能计算 ABABAB。
(m×n)⏟A⋅(n×p)⏟B=(m×p)⏟AB
\underbrace{(m \times n)}_{A} \cdot \underbrace{(n \times p)}_{B} = \underbrace{(m \times p)}_{AB}
A(m×n)⋅B(n×p)=AB(m×p)
也就是说,中间的两个 nnn 必须相等,乘完之后这两个 nnn “消失”,剩下外面的 mmm 和 ppp。
举个不能乘的例子:如果 AAA 是 3×23\times 23×2,BBB 也是 3×23 \times 23×2,那么 AAA 有 222 列,BBB 有 333 行,2≠32 \ne 32=3,所以 ABABAB 没有意义。但反过来 AAA 是 3×23\times 23×2、BBB 是 2×12\times 12×1(列向量)、或者 BBB 是 2×22\times 22×2、甚至 BBB 是 2×202 \times 202×20,都是可以乘的,因为 AAA 的列数(2)和 BBB 的行数都对上了。
一个简单的记忆口诀:“内侧数字要相等,外侧数字留下来”。

2. 矩阵加法——比乘法简单得多
在讲乘法之前先说一句加法。两个矩阵能相加,前提是形状完全相同(行数、列数都一样),加法就是对应位置的数字直接相加:
[1234]+[5678]=[681012]
\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} + \begin{bmatrix} 5 & 6 \\ 7 & 8 \end{bmatrix} = \begin{bmatrix} 6 & 8 \\ 10 & 12 \end{bmatrix}
[1324]+[5768]=[610812]
矩阵乘以一个常数 ccc,就是把每个数字都乘以 ccc:
2[1234]=[2468]
2 \begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix} = \begin{bmatrix} 2 & 4 \\ 6 & 8 \end{bmatrix}
2[1324]=[2648]
矩阵 −A-A−A 就是 c=−1c=-1c=−1 时的情况,所有符号取反。A+(−A)A + (-A)A+(−A) 得到的是零矩阵(所有元素都是 0)。这部分纯粹是常识,记住"形状必须一样"就行。
矩阵里第 iii 行第 jjj 列的那个数字,记作 aija_{ij}aij 或者 A(i,j)A(i,j)A(i,j)。比如第一行的元素依次是 a11,a12,…,a1na_{11}, a_{12}, \dots, a_{1n}a11,a12,…,a1n;左下角的元素是 am1a_{m1}am1,右下角是 amna_{mn}amn。
3. 矩阵乘法的第一种方式:点积法(逐个元素地算)
这是教科书里最常见、最标准的算法。
规则:ABABAB 的第 iii 行第 jjj 列那个数字,等于"AAA 的第 iii 行"和"BBB 的第 jjj 列"做点积(对应位置相乘再求和)。
(AB)ij=(A 的第 i 行)⋅(B 的第 j 列)
(AB)_{ij} = (\text{A 的第 } i \text{ 行}) \cdot (\text{B 的第 } j \text{ 列})
(AB)ij=(A 的第 i 行)⋅(B 的第 j 列)
写得更具体一点,如果 AAA 是 m×nm\times nm×n、BBB 是 n×pn \times pn×p:
(AB)ij=ai1b1j+ai2b2j+⋯+ainbnj=∑k=1naikbkj
(AB)_{ij} = a_{i1}b_{1j} + a_{i2}b_{2j} + \cdots + a_{in}b_{nj} = \sum_{k=1}^{n} a_{ik}b_{kj}
(AB)ij=ai1b1j+ai2b2j+⋯+ainbnj=k=1∑naikbkj

用一个具体例子感受一下
A=[1101],B=[2130]
A = \begin{bmatrix} 1 & 1 \\ 0 & 1 \end{bmatrix}, \quad B = \begin{bmatrix} 2 & 1 \\ 3 & 0 \end{bmatrix}
A=[1011],B=[2310]
算 (AB)11(AB)_{11}(AB)11(第1行第1列):取 AAA 第1行 (1,1)(1,1)(1,1),点乘 BBB 第1列 (2,3)(2,3)(2,3):
1×2+1×3=5
1\times 2 + 1 \times 3 = 5
1×2+1×3=5
按照同样的方法把另外三个位置也算出来,最终结果是:
AB=[5130]
AB = \begin{bmatrix} 5 & 1 \\ 3 & 0 \end{bmatrix}
AB=[5310]
这里一共算了 444 个点积,每个点积要做 222 次乘法,所以总共 888 次乘法。
两个极端情况
- 行向量 ×\times× 列向量:1×n1\times n1×n 乘以 n×1n \times 1n×1,结果是 1×11\times 11×1,也就是一个数字。这其实就是普通的"点积"(也叫"内积")。
- 列向量 ×\times× 行向量:n×1n \times 1n×1 乘以 1×n1 \times n1×n,结果是一整个 n×nn \times nn×n 矩阵!这叫"外积"。很多初学者会惊讶——两个向量也能乘出一整个矩阵。
一般情况要算多少次乘法?
如果 AAA 是 m×nm \times nm×n、BBB 是 n×pn \times pn×p,那么 ABABAB 一共有 m×pm \times pm×p 个位置,每个位置的点积需要 nnn 次乘法,所以总共需要
m×n×p 次乘法
m \times n \times p \text{ 次乘法}
m×n×p 次乘法
特别地,当 A,BA, BA,B 都是 n×nn\times nn×n 的方阵时,需要 n3n^3n3 次乘法。n=100n=100n=100 时大约要算一百万次乘法;n=2n=2n=2 时只要 23=82^3=823=8 次,正好对上前面的例子。
拓展小知识:数学家曾经以为 n=2n=2n=2 时 888 次乘法已经是极限了,后来斯特拉森(Strassen)发现用 777 次乘法(代价是多几次加法)也能算出同样的结果。把大矩阵切成 2×22\times 22×2 的小块反复用这个技巧,理论上能把 n3n^3n3 降到大约 n2.376n^{2.376}n2.376。但这类算法在实践中过于复杂,普通的科学计算还是老老实实用 n3n^3n3 的方法。
4. 矩阵乘法的第二种方式:逐列地看(A 乘 B 的每一列)
把 BBB 拆成一列一列来看。每一次,AAA 去乘 BBB 的一整列,得到的就是 ABABAB 的对应那一列。换句话说:
ABABAB 的每一列,都是 AAA 的各列的某种"组合"(线性组合),组合的系数就来自 BBB 的那一列。
举个例子,如果
A=[102135],B=[12] A = \begin{bmatrix} 1 & 0 \\ 2 & 1 \\ 3 & 5 \end{bmatrix}, \quad B = \begin{bmatrix} 1 \\ 2 \end{bmatrix} A=123015,B=[12]
那么 ABABAB(也就是 AAA 乘以这一列向量)等于:
1×[123]+2×[015]=[1413] 1 \times \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} + 2 \times \begin{bmatrix} 0 \\ 1 \\ 5 \end{bmatrix} = \begin{bmatrix} 1 \\ 4 \\ 13 \end{bmatrix} 1×123+2×015=1413
这就是"AAA 的第一列乘以 111,加上 AAA 的第二列乘以 222"。这个角度在后面学习"列空间"“线性组合”"线性方程组的解是否存在"时会反复用到,非常重要。
5. 矩阵乘法的第三种方式:逐行地看(A 的每一行乘 B)
这是上面那个角度的镜像。这次把 AAA 拆成一行一行,每一行(看成一个行向量)去乘整个矩阵 BBB,得到的就是 ABABAB 的对应那一行:
ABABAB 的每一行,都是 BBB 的各行的某种线性组合,组合系数来自 AAA 的那一行。
我们之前讲消元的时候,“用消元矩阵 EEE 左乘 AAA"其实就是这个角度的应用——EEE 的每一行决定了 EAEAEA 的对应行是 AAA 原来各行的什么组合(比如"第2行减去2倍第1行”)。
6. 矩阵乘法的第四种方式:列乘行,然后求和(很多人不知道的方式)
这是最容易被忽略、但其实非常优雅的一种方式。
做法:把 AAA 按列拆开,把 BBB 按行拆开。AAA 的第 kkk 列(一个 m×1m\times 1m×1 的列向量)乘以 BBB 的第 kkk 行(一个 1×p1\times p1×p 的行向量),这是一个"外积",结果是一整个 m×pm\times pm×p 的矩阵。把 k=1,2,…,nk=1,2,\dots,nk=1,2,…,n 这些外积矩阵全部加起来,就得到 ABABAB:
AB=(A 的第1列)(B 的第1行)+(A 的第2列)(B 的第2行)+⋯+(A 的第n列)(B 的第n行)
AB = (\text{A 的第1列})(\text{B 的第1行}) + (\text{A 的第2列})(\text{B 的第2行}) + \cdots + (\text{A 的第n列})(\text{B 的第n行})
AB=(A 的第1列)(B 的第1行)+(A 的第2列)(B 的第2行)+⋯+(A 的第n列)(B 的第n行)
用 2×22\times 22×2 矩阵验证一下
A=[abcd],B=[EFGH]
A = \begin{bmatrix} a & b \\ c & d \end{bmatrix}, \quad B = \begin{bmatrix} E & F \\ G & H \end{bmatrix}
A=[acbd],B=[EGFH]
按"列乘行"的方式:
AB=[ac][EF]+[bd][GH]=[aEaFcEcF]+[bGbHdGdH]=[aE+bGaF+bHcE+dGcF+dH]
AB = \begin{bmatrix} a \\ c \end{bmatrix}\begin{bmatrix} E & F \end{bmatrix} + \begin{bmatrix} b \\ d \end{bmatrix}\begin{bmatrix} G & H \end{bmatrix} = \begin{bmatrix} aE & aF \\ cE & cF \end{bmatrix} + \begin{bmatrix} bG & bH \\ dG & dH \end{bmatrix} = \begin{bmatrix} aE+bG & aF+bH \\ cE+dG & cF+dH \end{bmatrix}
AB=[ac][EF]+[bd][GH]=[aEcEaFcF]+[bGdGbHdH]=[aE+bGcE+dGaF+bHcF+dH]
跟用点积法算出来的结果完全一样!这说明四种方式只是看问题的角度不同,本质上算的是同一件事,用的乘法次数也完全一样(都是 mnpmnpmnp 次),只是把这些乘法重新归了类。


7. 四种方式的对比表
| 方式 | 怎么切 | 结果怎么来 | 一句话理解 |
|---|---|---|---|
| ① 点积法 | 逐个元素地算 | 行点乘列 | 教科书标准算法 |
| ② 列的视角 | 把 B 切成列 | A 乘 B 的每一列 | AB 的列是 A 的列的组合 |
| ③ 行的视角 | 把 A 切成行 | A 的每一行乘 B | AB 的行是 B 的行的组合 |
| ④ 列乘行求和 | A 切列、B 切行 | 外积矩阵相加 | 容易被忽视但很优雅 |
8. 矩阵乘法不满足交换律!
这是整节课最重要的提醒:一般来说 AB≠BAAB \ne BAAB=BA。
先说一个最直观的原因:形状可能对不上。如果 AAA 是 3×23\times 23×2、BBB 是 2×52 \times 52×5,那么 ABABAB 是 3×53\times 53×5,而 BABABA 根本无法计算(B 的列数是5,A 的行数是3,对不上)。即使两边都能算,结果的形状也可能不同。
就算是两个同样大小的方阵,ABABAB 和 BABABA 也几乎总是不相等。比如:
A=[0100],B=[1000]
A = \begin{bmatrix} 0 & 1 \\ 0 & 0 \end{bmatrix}, \quad B = \begin{bmatrix} 1 & 0 \\ 0 & 0 \end{bmatrix}
A=[0010],B=[1000]
AB=[0000],BA=[0100]
AB = \begin{bmatrix} 0 & 0 \\ 0 & 0 \end{bmatrix}, \quad BA = \begin{bmatrix} 0 & 1 \\ 0 & 0 \end{bmatrix}
AB=[0000],BA=[0010]
两者完全不同。所以做矩阵乘法的题目时,千万不能随便交换两个矩阵的相乘顺序。
但有一个特殊情况:单位矩阵 III 跟任何方阵都满足交换律,即 AI=IA=AAI = IA = AAI=IA=A。也只有形如 cIcIcI(数乘单位矩阵)的矩阵能跟所有矩阵交换。这是单位矩阵之所以特殊的原因之一。
9. 矩阵运算的六条法则
矩阵运算遵守一些和普通数字类似的法则,但也有一条经典法则被打破了。
加法的三条法则(都成立)
A+B=B+A(交换律)
A + B = B + A \quad (\text{交换律})
A+B=B+A(交换律)
c(A+B)=cA+cB(分配律)
c(A+B) = cA + cB \quad (\text{分配律})
c(A+B)=cA+cB(分配律)
A+(B+C)=(A+B)+C(结合律)
A + (B+C) = (A+B) + C \quad (\text{结合律})
A+(B+C)=(A+B)+C(结合律)
乘法的三条法则(交换律被打破!)
AB≠BA(乘法交换律一般不成立)
AB \ne BA \quad \text{(乘法交换律一般不成立)}
AB=BA(乘法交换律一般不成立)
A(B+C)=AB+AC(左分配律)
A(B+C) = AB + AC \quad (\text{左分配律})
A(B+C)=AB+AC(左分配律)
(A+B)C=AC+BC(右分配律)
(A+B)C = AC + BC \quad (\text{右分配律})
(A+B)C=AC+BC(右分配律)
A(BC)=(AB)C(结合律,括号可以随便挪)
A(BC) = (AB)C \quad (\text{结合律,括号可以随便挪})
A(BC)=(AB)C(结合律,括号可以随便挪)
最后这条结合律 A(BC)=(AB)CA(BC)=(AB)CA(BC)=(AB)C 非常重要,以至于书里专门给它起了名字:
矩阵乘法基本法则:AAA 乘以 BCBCBC,等于 ABABAB 乘以 CCC。换句话说,先算 BCBCBC 再左乘 AAA,和先算 ABABAB 再右乘 CCC,结果完全一样。
这条法则保证了我们可以放心地写 ABCABCABC 而不用加括号——不管你先算哪两个的乘积,最终结果都一样。它的证明思路是:分配律 A(B+C)=AB+ACA(B+C)=AB+ACA(B+C)=AB+AC 可以逐列证明(归结为 A(b+c)=Ab+AcA(b+c)=Ab+AcA(b+c)=Ab+Ac,这其实就是"线性"的核心定义),而结合律的证明虽然写起来比较繁琐,但用处极大,后面证明很多定理都要靠它。
10. 矩阵的幂
当 AAA 是方阵时,可以定义它的幂:
Ap=A⋅A⋅A⋯A⏟p 个 A 相乘
A^p = \underbrace{A \cdot A \cdot A \cdots A}_{p \text{ 个 } A \text{ 相乘}}
Ap=p 个 A 相乘A⋅A⋅A⋯A
矩阵的幂满足和普通数字幂一样的运算规则:
Ap⋅Aq=Ap+q,(Ap)q=Apq
A^p \cdot A^q = A^{p+q}, \qquad (A^p)^q = A^{pq}
Ap⋅Aq=Ap+q,(Ap)q=Apq
比如 A3⋅A4=A7A^3 \cdot A^4 = A^7A3⋅A4=A7(7个 AAA 连乘),而 (A3)4=A12(A^3)^4 = A^{12}(A3)4=A12(12个 AAA 连乘,不是 3+4=73+4=73+4=7,这一点要小心区分)。
当 p,qp, qp,q 可以是零或负数时,上面的规则依然成立,前提是 AAA 存在"−1-1−1 次方",也就是逆矩阵 A−1A^{-1}A−1。这时约定 A0=IA^0 = IA0=I(类比普通数字里 20=12^0=120=1)。
注意:矩阵的逆写作 A−1A^{-1}A−1,但绝不能写成 1/A1/A1/A 或者 1A\dfrac{1}{A}A1(除了在 MATLAB 这种软件的特殊语法里)。不是所有矩阵都有逆,什么时候 AAA 可逆是下一节(2.5节)要讨论的核心问题。
11. 分块矩阵与分块乘法
一个大矩阵可以被"切"成若干个小矩阵(称为"块",block),只要切法合理,运算时可以把每一块当成一个数字来处理。
直观例子
下面是一个 4×64\times 64×6 的矩阵,按照 2×22\times 22×2 切成小块,刚好每一块都是单位矩阵 III:
A=[101010010101101010010101]=[IIIIII]
A = \left[\begin{array}{cc:cc:cc}
1&0&1&0&1&0\\
0&1&0&1&0&1\\ \hdashline
1&0&1&0&1&0\\
0&1&0&1&0&1
\end{array}\right] = \begin{bmatrix} I & I & I \\ I & I & I \end{bmatrix}
A=101001011010010110100101=[IIIIII]
切完之后,这个 4×64\times 64×6 的矩阵变成了一个 2×32\times 32×3 的"块矩阵",每一块都是 2×22\times 22×2 的单位矩阵。显然,这个块矩阵的表达比原始的一堆 0、1 数字清楚多了。
分块加法
如果 AAA 和 BBB 形状相同、并且切法(块的大小)也对应一致,那么可以逐块相加,就像普通矩阵加法一样。
分块乘法的规则
规则:只要 AAA 的"列方向的切法"和 BBB 的"行方向的切法"匹配,就可以把每一块当成一个数字来做矩阵乘法。
举个最重要的特例:把 AAA 按"列"切成 nnn 块(每块是一整列),把 BBB 按"行"切成 nnn 块(每块是一整行)。这正是前面讲的第四种乘法方式——分块乘法和"列乘行求和"本质上是一回事:
AB=∑k=1n(A 的第 k 列)(B 的第 k 行)
AB = \sum_{k=1}^{n} (\text{A 的第 } k \text{ 列})(\text{B 的第 } k \text{ 行})
AB=k=1∑n(A 的第 k 列)(B 的第 k 行)
应用:分块做消元(Schur 补)
假设一个矩阵被分成四块 A,B,C,DA, B, C, DA,B,C,D:
M=[ABCD]
M = \begin{bmatrix} A & B \\ C & D \end{bmatrix}
M=[ACBD]
我们想通过"消元"把左下角的块 CCC 变成零块,方法是用 CA−1CA^{-1}CA−1(相当于普通消元里的"乘数 c/ac/ac/a")去乘第一行的块 [A B][A\ \ B][A B],然后从第二行块中减去:
[ABCD]⟶[AB0D−CA−1B]
\begin{bmatrix} A & B \\ C & D \end{bmatrix} \longrightarrow \begin{bmatrix} A & B \\ 0 & D - CA^{-1}B \end{bmatrix}
[ACBD]⟶[A0BD−CA−1B]
这里 CCC 被消成了零块,而右下角变成了 D−CA−1BD - CA^{-1}BD−CA−1B,这个组合有个专门的名字,叫 Schur 补(Schur complement)。它和普通消元中"主元下面那个数字变成 d−cabd - \dfrac{c}{a}bd−acb"的公式是完全平行的,只不过这里的 a,b,c,da,b,c,da,b,c,d 换成了矩阵块。

12. 应用实例:用矩阵数图中的路径条数
这是一个非常生动的应用,能帮你理解矩阵乘法"到底在干什么"。
考虑一个有 444 个节点的无向图,节点之间用边连接:
| 边的连接情况 | 1-2 | 1-3 | 2-3 | 2-4 | 3-4 |
|---|---|---|---|---|---|
| 是否相连 | 是 | 是 | 是 | 是 | 是 |
用 mermaid 把这张图画出来:
这张图对应一个 4×44\times 44×4 的邻接矩阵 SSS:如果节点 iii 和节点 jjj 之间有边,那么 Sij=1S_{ij}=1Sij=1,否则 Sij=0S_{ij}=0Sij=0。因为是"无向图"(边没有方向),所以 SSS 是对称矩阵(Sij=SjiS_{ij}=S_{ji}Sij=Sji)。根据上面的连接关系:
S=[0110101111010110]
S = \begin{bmatrix}
0 & 1 & 1 & 0 \\
1 & 0 & 1 & 1 \\
1 & 1 & 0 & 1 \\
0 & 1 & 1 & 0
\end{bmatrix}
S=0110101111010110
关键问题:S2S^2S2 是什么意思?
按照点积法的定义:
(S2)ij=(S 的第 i 行)⋅(S 的第 j 列)=∑k=14SikSkj
(S^2)_{ij} = (\text{S 的第 } i \text{ 行}) \cdot (\text{S 的第 } j \text{ 列}) = \sum_{k=1}^{4} S_{ik}S_{kj}
(S2)ij=(S 的第 i 行)⋅(S 的第 j 列)=k=1∑4SikSkj
每一项 SikSkjS_{ik}S_{kj}SikSkj 只有当 Sik=1S_{ik}=1Sik=1 且 Skj=1S_{kj}=1Skj=1 时才等于 111,也就是"iii 到 kkk 有边,并且 kkk 到 jjj 也有边"——这恰好就是一条"经过中间节点 kkk、长度为 222"的路径!把 kkk 取遍所有节点再求和,(S2)ij(S^2)_{ij}(S2)ij 算出来的就是:
从节点 iii 到节点 jjj、恰好走 222 步的路径条数。
比如节点 222 到节点 333:可以走 “2→1→3” 或者 “2→4→3”,所以应该有 222 条长度为 2 的路径。
同样的道理可以推广:SNS^NSN 的 (i,j)(i,j)(i,j) 位置,就是从节点 iii 到节点 jjj 恰好走 NNN 步的路径条数,因为一条 NNN 步的路径,可以看成"先走 N−1N-1N−1 步到达某个中间节点 kkk,再从 kkk 走最后一步到 jjj"——这正好对应了矩阵乘法 SN=SN−1⋅SS^N = S^{N-1}\cdot SSN=SN−1⋅S 的点积定义。
这就是为什么"矩阵乘法"天然适合用来数图上的路径——无论是社交网络里"认识的人的朋友",还是公司里员工之间的沟通渠道,背后都是同一套数学。
13. 完整可运行的 C++ 代码
下面这份代码完整实现了本节讲到的核心内容,包括:
- 用点积法(第一种方式)实现矩阵乘法;
- 验证矩阵乘法结合律 A(BC)=(AB)CA(BC) = (AB)CA(BC)=(AB)C;
- 验证矩阵乘法一般不满足交换律 AB≠BAAB \ne BAAB=BA;
- 用列乘行求和(第四种方式)实现矩阵乘法,并与点积法结果做对比;
- 用邻接矩阵的幂 S2S^2S2 来数图上长度为 2 的路径条数。
代码不依赖<bits/stdc++.h>,只用标准库里明确需要的头文件,可以直接用g++编译运行。
// ===================================================================
// 文件名: matrix_rules_demo.cpp
// 功能 : 演示《线性代数》2.4节"矩阵运算的规则"中的核心知识点
// 1) 点积法做矩阵乘法
// 2) 验证结合律 A(BC) = (AB)C
// 3) 验证乘法一般不满足交换律 AB != BA
// 4) 列乘行求和的方式做矩阵乘法,并与点积法对比
// 5) 用邻接矩阵的平方 S^2 计算图中长度为2的路径条数
// 编译方式: g++ -std=c++17 -O2 matrix_rules_demo.cpp -o matrix_rules_demo
// 运行方式: ./matrix_rules_demo
// ===================================================================
#include <iostream> // 用于标准输入输出 std::cout
#include <vector> // 用于 std::vector 容器,存放矩阵的数据
#include <stdexcept> // 用于抛出异常 std::invalid_argument
#include <iomanip> // 用于控制输出格式(比如设置打印宽度)
#include <cmath> // 用于 std::abs,比较浮点数是否相等时要用到
// -------------------------------------------------------------------
// 用一个类来表示矩阵,内部用 二维 vector<vector<double>> 存储数据
// 这样写的好处是:可以重载运算符,让矩阵乘法、加法看起来和数学公式一样自然
// -------------------------------------------------------------------
class Matrix {
public:
// rows_: 矩阵的行数;cols_: 矩阵的列数
// data_: 实际存放矩阵元素的二维数组,data_[i][j] 就是第 i 行第 j 列的元素
int rows_;
int cols_;
std::vector<std::vector<double>> data_;
// 构造函数:创建一个 r 行 c 列、所有元素初始化为 0 的矩阵
Matrix(int r, int c) : rows_(r), cols_(c) {
// 用 r 个长度为 c 的向量来初始化二维数组,初始值全部为 0.0
data_.assign(r, std::vector<double>(c, 0.0));
}
// 用一个二维初始化列表直接构造矩阵,方便我们在 main 函数里快速写出矩阵
// 例如: Matrix A({{1,2},{3,4}}); 就表示矩阵 [[1,2],[3,4]]
Matrix(std::initializer_list<std::vector<double>> init) {
data_ = init; // 把初始化列表转成 vector<vector<double>>
rows_ = (int)data_.size(); // 行数就是外层 vector 的大小
cols_ = rows_ > 0 ? (int)data_[0].size() : 0; // 列数取第一行的长度
}
// 重载 () 运算符,方便用 A(i, j) 的方式访问/修改第 i 行第 j 列的元素
// 注意:这里用的是数学习惯,下标从 0 开始(程序员视角),
// 而不是教材里从 1 开始的写法,使用时要留意这一点差异
double& operator()(int i, int j) {
return data_[i][j];
}
const double& operator()(int i, int j) const {
return data_[i][j];
}
// 打印矩阵,方便调试和展示结果
void print(const std::string& name) const {
std::cout << name << " (" << rows_ << "x" << cols_ << ") = " << std::endl;
for (int i = 0; i < rows_; ++i) {
std::cout << " [ ";
for (int j = 0; j < cols_; ++j) {
// setw(6) 让每个数字占6个字符宽度,方便对齐
std::cout << std::setw(6) << data_[i][j] << " ";
}
std::cout << "]" << std::endl;
}
}
};
// -------------------------------------------------------------------
// 函数: multiplyDotProduct
// 功能: 用"点积法"(教材里的第一种方式)计算矩阵乘法 A * B
// 即 (AB)_{ij} = (A 的第 i 行) 点乘 (B 的第 j 列)
// -------------------------------------------------------------------
Matrix multiplyDotProduct(const Matrix& A, const Matrix& B) {
// 先检查维度是否匹配:A 的列数必须等于 B 的行数
if (A.cols_ != B.rows_) {
throw std::invalid_argument("维度不匹配,无法相乘:A的列数必须等于B的行数");
}
// 结果矩阵 C 的大小是 (A的行数) x (B的列数)
Matrix C(A.rows_, B.cols_);
// 三层循环:i 遍历 C 的每一行,j 遍历 C 的每一列,k 用来做点积求和
for (int i = 0; i < A.rows_; ++i) {
for (int j = 0; j < B.cols_; ++j) {
double sum = 0.0; // 用来累加点积的结果
for (int k = 0; k < A.cols_; ++k) {
// 点积公式: sum += a_{ik} * b_{kj}
sum += A(i, k) * B(k, j);
}
C(i, j) = sum; // 把算好的点积结果放进 C 的第 i 行第 j 列
}
}
return C;
}
// -------------------------------------------------------------------
// 函数: multiplyColumnTimesRow
// 功能: 用"列乘行求和"(教材里的第四种方式)计算矩阵乘法 A * B
// 即 AB = sum_{k=1}^{n} (A的第k列) * (B的第k行)
// 这里每一项 (A的第k列)*(B的第k行) 都是一个"外积"矩阵
// -------------------------------------------------------------------
Matrix multiplyColumnTimesRow(const Matrix& A, const Matrix& B) {
if (A.cols_ != B.rows_) {
throw std::invalid_argument("维度不匹配,无法相乘:A的列数必须等于B的行数");
}
int n = A.cols_; // n 是 A 的列数,也是 B 的行数
Matrix C(A.rows_, B.cols_); // 用来累加所有外积矩阵的结果,初始为全0
// 对每一个 k,把 "A的第k列" 和 "B的第k行" 做外积,再累加进 C
for (int k = 0; k < n; ++k) {
for (int i = 0; i < A.rows_; ++i) {
for (int j = 0; j < B.cols_; ++j) {
// 外积矩阵的 (i,j) 元素就是 A(i,k) * B(k,j)
// 把它累加到结果矩阵对应位置上
C(i, j) += A(i, k) * B(k, j);
}
}
}
return C;
}
// -------------------------------------------------------------------
// 函数: isApproximatelyEqual
// 功能: 比较两个矩阵是否(在浮点误差范围内)相等,用于验证不同算法结果一致
// -------------------------------------------------------------------
bool isApproximatelyEqual(const Matrix& A, const Matrix& B, double eps = 1e-9) {
if (A.rows_ != B.rows_ || A.cols_ != B.cols_) return false;
for (int i = 0; i < A.rows_; ++i) {
for (int j = 0; j < A.cols_; ++j) {
// 如果对应位置的差值的绝对值超过 eps,认为不相等
if (std::abs(A(i, j) - B(i, j)) > eps) return false;
}
}
return true;
}
// -------------------------------------------------------------------
// 函数: matrixPower
// 功能: 计算方阵 A 的 N 次幂 A^N(N 是非负整数),用于后面数路径条数
// 做法很简单:从单位矩阵开始,循环乘 N 次 A
// -------------------------------------------------------------------
Matrix matrixPower(const Matrix& A, int N) {
int n = A.rows_;
// 构造一个 n x n 的单位矩阵,作为乘法的初始值(相当于数字里的"1")
Matrix result(n, n);
for (int i = 0; i < n; ++i) {
result(i, i) = 1.0;
}
// 循环做 N 次矩阵乘法: result = result * A
for (int t = 0; t < N; ++t) {
result = multiplyDotProduct(result, A);
}
return result;
}
// ===================================================================
// 主函数:依次演示前面讲到的知识点
// ===================================================================
int main() {
std::cout << "========================================" << std::endl;
std::cout << "演示1: 用点积法计算矩阵乘法 AB" << std::endl;
std::cout << "========================================" << std::endl;
// 对应讲解文档里第3节的例子: A = [[1,1],[0,1]], B = [[2,1],[3,0]]
Matrix A({{1, 1},
{0, 1}});
Matrix B({{2, 1},
{3, 0}});
Matrix AB = multiplyDotProduct(A, B);
A.print("A");
B.print("B");
AB.print("AB (点积法计算)");
// 预期结果应该是 [[5,1],[3,0]],与讲解文档里的手算结果一致
std::cout << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "演示2: 验证结合律 A(BC) = (AB)C" << std::endl;
std::cout << "========================================" << std::endl;
// 再随便造一个矩阵 C,用于验证结合律
Matrix C({{1, 0, 2},
{0, 1, 1}});
// 先算 (AB)C:A是2x2,B是2x2,AB是2x2;再乘C(2x3),得到(AB)C是2x3
Matrix AB_then_C = multiplyDotProduct(multiplyDotProduct(A, B), C);
// 再算 A(BC):B是2x2,C是2x3,BC是2x3;再用A(2x2)左乘,得到A(BC)是2x3
Matrix BC = multiplyDotProduct(B, C);
Matrix A_then_BC = multiplyDotProduct(A, BC);
AB_then_C.print("(AB)C");
A_then_BC.print("A(BC)");
if (isApproximatelyEqual(AB_then_C, A_then_BC)) {
std::cout << ">>> 验证成功: (AB)C 和 A(BC) 完全相等,结合律成立!" << std::endl;
} else {
std::cout << ">>> 出现错误: 两者不相等,请检查代码。" << std::endl;
}
std::cout << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "演示3: 验证矩阵乘法一般不满足交换律 AB != BA" << std::endl;
std::cout << "========================================" << std::endl;
Matrix BA = multiplyDotProduct(B, A);
AB.print("AB");
BA.print("BA");
if (!isApproximatelyEqual(AB, BA)) {
std::cout << ">>> 正如预期: AB 和 BA 不相等,矩阵乘法不满足交换律。" << std::endl;
} else {
std::cout << ">>> 这一组矩阵恰好满足 AB = BA(这是巧合,不是一般规律)。" << std::endl;
}
std::cout << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "演示4: 用'列乘行求和'方式重新计算AB,并与点积法对比" << std::endl;
std::cout << "========================================" << std::endl;
Matrix AB_columnRow = multiplyColumnTimesRow(A, B);
AB_columnRow.print("AB (列乘行求和法计算)");
if (isApproximatelyEqual(AB, AB_columnRow)) {
std::cout << ">>> 验证成功: 两种方法算出的AB完全一致!" << std::endl;
} else {
std::cout << ">>> 出现错误: 两种方法结果不一致,请检查代码。" << std::endl;
}
std::cout << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "演示5: 用邻接矩阵的平方 S^2 数图中长度为2的路径条数" << std::endl;
std::cout << "========================================" << std::endl;
// 4个节点的无向图,对应讲解文档第12节的例子
// 节点编号 0,1,2,3 对应教材里的节点 1,2,3,4
// 边: 1-2, 1-3, 2-3, 2-4, 3-4
Matrix S({{0, 1, 1, 0},
{1, 0, 1, 1},
{1, 1, 0, 1},
{0, 1, 1, 0}});
Matrix S2 = matrixPower(S, 2); // 计算 S 的平方
S.print("邻接矩阵 S");
S2.print("S^2 (每个位置是对应两点间长度为2的路径条数)");
// 单独取出节点2到节点3的路径条数(注意下标从0开始,所以是 S2(1,2))
std::cout << "节点2到节点3之间,长度为2的路径条数 = "
<< S2(1, 2) << " (应该等于2: 2->1->3 和 2->4->3)" << std::endl;
return 0;
}
代码运行结果说明
编译运行之后,你会看到:
- 演示1 会算出 AB=[5130]AB=\begin{bmatrix}5&1\\3&0\end{bmatrix}AB=[5310],和文档第3节手算的结果完全一致;
- 演示2 会确认 (AB)C(AB)C(AB)C 与 A(BC)A(BC)A(BC) 是同一个矩阵,验证了结合律;
- 演示3 会展示 AB≠BAAB \ne BAAB=BA(在大多数随便选的矩阵下都是这样);
- 演示4 会用"列乘行求和"重新算一遍 ABABAB,并确认和点积法结果一致,说明四种乘法方式只是计算顺序不同,结果相同;
- 演示5 会算出 S2S^2S2,并验证节点2到节点3之间恰好有2条长度为2的路径,对应"2→1→3"和"2→4→3"这两条路。
14. 用 ASCII 图直观感受"分块矩阵"的结构
下面用纯文本画出"分块矩阵"是怎么把一个大矩阵切成小矩阵的(对应第11节的例子,一个4×6矩阵切成2×3个小块,每块都是2×2的单位矩阵):
原始的 4 行 6 列矩阵 A:
列1 列2 列3 列4 列5 列6
行1 [ 1 0 | 1 0 | 1 0 ]
行2 [ 0 1 | 0 1 | 0 1 ]
------------------------------------ <- 横向切割线(行方向)
行3 [ 1 0 | 1 0 | 1 0 ]
行4 [ 0 1 | 0 1 | 0 1 ]
^ ^
竖向切割线 竖向切割线(列方向)
切完之后,看成一个 2 行 3 列的"块矩阵":
块列1 块列2 块列3
块行1 [ I I I ]
块行2 [ I I I ]
其中每个 I 都代表一个 2x2 的单位矩阵:
I = [ 1 0 ]
[ 0 1 ]
15. 小结(本节核心要点回顾)
| 编号 | 要点 |
|---|---|
| 1 | ABABAB 能算的前提:AAA 的列数 = BBB 的行数 |
| 2 | (AB)ij(AB)_{ij}(AB)ij = (AAA 第 iii 行) 点乘 (BBB 第 jjj 列) |
| 3 | m×nm\times nm×n 乘 n×pn\times pn×p 总共需要 mnpmnpmnp 次乘法 |
| 4 | A(BC)=(AB)CA(BC)=(AB)CA(BC)=(AB)C,这是矩阵乘法最重要的法则 |
| 5 | ABABAB 也等于 nnn 个外积矩阵之和:(A 的第k列)(B 的第k行) 加总 |
| 6 | 分块乘法:只要切法匹配,把每块当数字处理即可 |
| 7 | 分块消元会产生 Schur 补 D−CA−1BD - CA^{-1}BD−CA−1B |
| 8 | 矩阵乘法一般不满足交换律:AB≠BAAB \neq BAAB=BA |
2.5 逆矩阵(Inverse Matrices)—— 从零理解
一、什么是逆矩阵?先从"做和撤销"说起
想象你穿上了袜子,又穿上了鞋子。如果你要"撤销"这个过程,必须先脱鞋,再脱袜子——顺序是反过来的。
矩阵的逆,做的就是类似的事:如果矩阵 AAA 对向量 xxx 做了某种操作变成了 bbb(写成 Ax=bAx=bAx=b),那么逆矩阵 A−1A^{-1}A−1 的作用就是把这个操作"撤销",让你重新得到 xxx。
定义:对一个方阵 AAA,如果存在一个矩阵 A−1A^{-1}A−1,满足
A−1A=I且AA−1=I
A^{-1}A = I \qquad \text{且} \qquad AA^{-1} = I
A−1A=I且AA−1=I
那么就说 AAA 是"可逆的"(invertible),A−1A^{-1}A−1 叫做 AAA 的逆矩阵。这里的 III 是单位矩阵——它对任何向量什么都不做,乘了等于没乘:Ix=xIx = xIx=x。
直观地说:
A−1Ax=x
A^{-1}Ax = x
A−1Ax=x
无论 AAA 把 xxx 变成了什么,A−1A^{-1}A−1 都能把它变回原来的 xxx。
类比数字:一个数 aaa 的逆就是 1a\dfrac{1}{a}a1,只要 a≠0a \neq 0a=0。矩阵的逆是同样的道理,只不过矩阵比数字复杂得多——不是所有方阵都有逆,这正是这一节要研究的核心问题。

二、不是所有矩阵都可逆!怎么判断?
这一节给出了好几种判断方法(书里称为"六个注记"),它们其实都在回答同一个问题:AAA 有没有逆?
判断方法汇总表
| 判断方法 | 具体内容 |
|---|---|
| 消元法(最常用) | 消元后必须得到 nnn 个非零主元(pivot),允许换行 |
| 行列式法 | det(A)≠0\det(A) \neq 0det(A)=0 |
| 方程法 | Ax=0Ax=0Ax=0 只能有零解 x=0x=0x=0 |
| 2×2 公式法 | ad−bc≠0ad-bc \neq 0ad−bc=0 |
| 对角占优法 | 每行对角线上的数字"压得住"该行其他数字之和 |
下面逐一讲清楚每一种方法背后的道理。
方法 1:消元主元法(Note 1)
对一个 n×nn\times nn×n 的方阵做消元(高斯消元),如果最终能得到 nnn 个非零的主元(对角线上的关键数字),那么这个矩阵就一定可逆。换行(行交换)是允许的,不影响这个结论。
这是实际计算中最常用、最直接的判断方式——你甚至不需要算出逆矩阵,只要做消元看主元个数就行。
方法 2:逆矩阵是唯一的(Note 2)
一个矩阵不可能有两个不同的逆。证明很巧妙,靠的是"括号搬家":
假设 BA=IBA = IBA=I(BBB 是左逆)并且 AC=IAC = IAC=I(CCC 是右逆)。考虑乘积 BACBACBAC,可以从两个方向去算括号:
B(AC)=(BA)C
B(AC) = (BA)C
B(AC)=(BA)C
左边 B(AC)=BI=BB(AC) = BI = BB(AC)=BI=B,右边 (BA)C=IC=C(BA)C = IC = C(BA)C=IC=C。所以:
B=C
B = C
B=C
这说明:左逆和右逆必须是同一个矩阵。这就是为什么我们可以放心地说"AAA 的逆",而不必区分"左边的逆"和"右边的逆"。
方法 3:用逆矩阵直接解方程(Note 3)
如果 AAA 可逆,那么方程 Ax=bAx=bAx=b 有且只有一个解:
x=A−1b
x = A^{-1}b
x=A−1b
这是逆矩阵最直接的用途:两边同时左乘 A−1A^{-1}A−1,左边 A−1Ax=Ix=xA^{-1}Ax = Ix = xA−1Ax=Ix=x,右边变成 A−1bA^{-1}bA−1b。
但实际计算中很少真的去求 A−1A^{-1}A−1——直接用消元法解 Ax=bAx=bAx=b 通常更快(后面会算一下两者的计算量差异)。
方法 4:零向量检测法(Note 4,非常重要)
关键结论:如果存在一个非零向量 xxx,使得 Ax=0Ax = 0Ax=0,那么 AAA 绝对不可能有逆。
为什么?因为零向量 000 经过 AAA 变换后还是 000(A⋅0=0A\cdot 0 = 0A⋅0=0),但现在又有一个非零的 xxx 也变成了 000。这就好比两条不同的路都通向了同一个终点——如果 A−1A^{-1}A−1 存在,它要怎么知道"把 000 变回去"应该变成 xxx 还是变成 000 呢?它做不到,所以 A−1A^{-1}A−1 不存在。
反过来说:如果 AAA 真的可逆,那么 Ax=0Ax=0Ax=0 就只能有唯一解 x=A−10=0x = A^{-1}0 = 0x=A−10=0。

方法 5:2×2 矩阵的行列式公式(Note 5)
对于最简单的 2×2 矩阵:
A=[abcd]
A = \begin{bmatrix} a & b \\ c & d \end{bmatrix}
A=[acbd]
它可逆的条件是 ad−bc≠0ad - bc \neq 0ad−bc=0,并且逆矩阵有现成公式:
A−1=1ad−bc[d−b−ca]
A^{-1} = \frac{1}{ad-bc}\begin{bmatrix} d & -b \\ -c & a \end{bmatrix}
A−1=ad−bc1[d−c−ba]
这里 ad−bcad-bcad−bc 这个数,就叫做 AAA 的行列式(determinant),记作 det(A)\det(A)det(A)。注意公式里要拿这个数做分母,所以一旦它是 000,就没法除,矩阵自然没有逆。
举例:矩阵 A=[1224]A=\begin{bmatrix}1&2\\2&4\end{bmatrix}A=[1224] 不可逆,因为 ad−bc=1×4−2×2=0ad-bc = 1\times4 - 2\times2 = 0ad−bc=1×4−2×2=0。同时也能验证:取 x=(2,−1)x=(2,-1)x=(2,−1),算一下 AxAxAx:
Ax=[1224][2−1]=[1×2+2×(−1)2×2+4×(−1)]=[00]
Ax = \begin{bmatrix}1&2\\2&4\end{bmatrix}\begin{bmatrix}2\\-1\end{bmatrix} = \begin{bmatrix}1\times2+2\times(-1)\\2\times2+4\times(-1)\end{bmatrix} = \begin{bmatrix}0\\0\end{bmatrix}
Ax=[1224][2−1]=[1×2+2×(−1)2×2+4×(−1)]=[00]
正好对应方法 4:存在非零向量 x=(2,−1)x=(2,-1)x=(2,−1) 使得 Ax=0Ax=0Ax=0,所以不可逆。三种判断方法(行列式、零向量、主元个数)在这个例子里完全一致地给出"不可逆"的结论。
方法 6:对角矩阵(Note 6)
对角矩阵(只有主对角线上有非零数字,其余全是 000)只要对角线上没有 000,就一定可逆,而且逆矩阵也很简单——把每个对角元素取倒数即可。
方法补充:对角占优矩阵一定可逆
如果一个矩阵每一行里,对角线上那个数的绝对值,比该行其余所有数的绝对值之和还大,即:
∣aii∣>∑j≠i∣aij∣
|a_{ii}| > \sum_{j\neq i} |a_{ij}|
∣aii∣>j=i∑∣aij∣
那么这个矩阵叫"对角占优"(diagonally dominant),它一定可逆,不需要再去算行列式或做消元验证。
直观证明思路:假设有非零向量 xxx,设它绝对值最大的分量是 xix_ixi。如果 Ax=0Ax=0Ax=0,那么 AxAxAx 的第 iii 行展开后,aiixia_{ii}x_iaiixi 这一项的绝对值,会比同一行其他所有项加起来的绝对值还大(因为对角占优),所以它们根本不可能正好抵消为 000。这就矛盾了,所以只能 x=0x=0x=0,从而 AAA 可逆。
三、两个矩阵乘积的逆——顺序要反过来!
直观类比
还是穿鞋子的例子:先穿袜子(动作 BBB),再穿鞋子(动作 AAA),合起来的动作是"先 BBB 后 AAA",写成矩阵形式就是 ABABAB(矩阵作用在最右边的向量上,所以最先生效的写在右边)。
撤销这个过程,必须先脱鞋(撤销 AAA,也就是 A−1A^{-1}A−1),再脱袜子(撤销 BBB,也就是 B−1B^{-1}B−1)。所以:
(AB)−1=B−1A−1
(AB)^{-1} = B^{-1}A^{-1}
(AB)−1=B−1A−1
注意顺序反过来了! 不是 A−1B−1A^{-1}B^{-1}A−1B−1。
严格证明
要证明 B−1A−1B^{-1}A^{-1}B−1A−1 确实是 ABABAB 的逆,只需要把它们乘起来,看是否等于 III:
(AB)(B−1A−1)=A(BB−1)A−1=A⋅I⋅A−1=AA−1=I
(AB)(B^{-1}A^{-1}) = A(BB^{-1})A^{-1} = A\cdot I \cdot A^{-1} = AA^{-1} = I
(AB)(B−1A−1)=A(BB−1)A−1=A⋅I⋅A−1=AA−1=I
这里的关键技巧是"挪动括号":先把中间的 BB−1BB^{-1}BB−1 消成 III,外面的 AAA 和 A−1A^{-1}A−1 自然就挨在一起,消成 III。
同样可以验证反过来乘也成立:(B−1A−1)(AB)=I(B^{-1}A^{-1})(AB) = I(B−1A−1)(AB)=I。所以 B−1A−1B^{-1}A^{-1}B−1A−1 确实是 (AB)−1(AB)^{-1}(AB)−1。
这个规律可以推广到任意多个矩阵相乘:
(ABC)−1=C−1B−1A−1
(ABC)^{-1} = C^{-1}B^{-1}A^{-1}
(ABC)−1=C−1B−1A−1

一个很微妙的例子:消元矩阵的顺序问题
设 EEE 是"用第1行消去第2行"的消元矩阵(把第2行减去5倍第1行),FFF 是"用第2行消去第3行"的消元矩阵(把第3行减去4倍第2行):
E=[100−510001],E−1=[100510001]
E = \begin{bmatrix}1&0&0\\-5&1&0\\0&0&1\end{bmatrix},\qquad
E^{-1} = \begin{bmatrix}1&0&0\\5&1&0\\0&0&1\end{bmatrix}
E=1−50010001,E−1=150010001
F=[1000100−41],F−1=[100010041]
F = \begin{bmatrix}1&0&0\\0&1&0\\0&-4&1\end{bmatrix},\qquad
F^{-1} = \begin{bmatrix}1&0&0\\0&1&0\\0&4&1\end{bmatrix}
F=10001−4001,F−1=100014001
现在分别计算 FEFEFE 和 E−1F−1E^{-1}F^{-1}E−1F−1,会发现一个很有意思的现象:
- FEFEFE(先做 E 再做 F 的消元顺序):第3行会受到第1行的"二次影响"——因为 FFF 是在 EEE 已经改变了第2行之后才作用的,所以 FEFEFE 里会出现一个额外的数字 “20”(5×45\times45×4)。
- E−1F−1E^{-1}F^{-1}E−1F−1(撤销的顺序,先撤 F 再撤 E):因为 F−1F^{-1}F−1 先把第3行加回去,这时候第2行还没被 E−1E^{-1}E−1 改变,所以不会产生这个"20",第3行不会感受到第1行的二次影响。
这正是为什么后面学习 A=LUA=LUA=LU 分解时,要用 E−1F−1⋯E^{-1}F^{-1}\cdotsE−1F−1⋯ 这种"逆序乘起来"的方式——因为这样得到的下三角矩阵 LLL 里,消元乘数会非常整齐地"原样落位",没有交叉污染的多余项。
四、怎么算出逆矩阵?——高斯-若尔当消元法(Gauss-Jordan)
基本思路
我们想求解 AA−1=IAA^{-1} = IAA−1=I。把这件事拆开看:A−1A^{-1}A−1 的每一列乘上 AAA,应该得到单位矩阵 III 对应的那一列。
比如 AAA 是 3×33\times33×3 矩阵,设 A−1A^{-1}A−1 的三列分别是 x1,x2,x3x_1, x_2, x_3x1,x2,x3,那么:
Ax1=e1=(100),Ax2=e2=(010),Ax3=e3=(001)
Ax_1 = e_1 = \begin{pmatrix}1\\0\\0\end{pmatrix},\quad
Ax_2 = e_2 = \begin{pmatrix}0\\1\\0\end{pmatrix},\quad
Ax_3 = e_3 = \begin{pmatrix}0\\0\\1\end{pmatrix}
Ax1=e1=100,Ax2=e2=010,Ax3=e3=001
也就是说,求逆矩阵其实是同时解 nnn 个方程组,而且它们的系数矩阵都是同一个 AAA。
具体做法:增广矩阵 [A∣I][A \mid I][A∣I]
把 AAA 和单位矩阵 III 拼在一起,写成增广矩阵 [A∣I][A \mid I][A∣I],然后对整行做消元操作(高斯消元 + 继续往上消,即"若尔当"的贡献),目标是把左边的 AAA 变成 III。等左边变成 III 之后,右边自然就变成了 A−1A^{-1}A−1:
[A∣I]→消元操作[I∣A−1]
[A \mid I] \xrightarrow{\text{消元操作}} [I \mid A^{-1}]
[A∣I]消元操作[I∣A−1]
完整例子:求矩阵 KKK 的逆
书中用到的经典例子:
K=[2−10−12−10−12]
K = \begin{bmatrix} 2&-1&0\\-1&2&-1\\0&-1&2\end{bmatrix}
K=2−10−12−10−12
第一步:写出增广矩阵
[K∣I]=[2−10100−12−10100−12001]
[K \mid I] = \left[\begin{array}{ccc|ccc} 2&-1&0&1&0&0\\-1&2&-1&0&1&0\\0&-1&2&0&0&1\end{array}\right]
[K∣I]=2−10−12−10−12100010001
第二步:正向消元(变成上三角,这是高斯的部分)
把第1行的 12\frac1221 倍加到第2行(消去第2行第1列的 −1-1−1):
[2−10100032−112100−12001]
\left[\begin{array}{ccc|ccc} 2&-1&0&1&0&0\\0&\frac32&-1&\frac12&1&0\\0&-1&2&0&0&1\end{array}\right]
200−123−10−121210010001
再把新第2行的 23\frac2332 倍加到第3行(消去第3行第2列的 −1-1−1):
[2−10100032−11210004313231]
\left[\begin{array}{ccc|ccc} 2&-1&0&1&0&0\\0&\frac32&-1&\frac12&1&0\\0&0&\frac43&\frac13&\frac23&1\end{array}\right]
200−12300−134121310132001
主元是 2, 32, 432,\ \frac32,\ \frac432, 23, 34,左边三列已经是上三角矩阵 UUU 了。
第三步:反向消元,往上消零(这是若尔当多走的一步)
用第3行把第2行、第1行里第3列的非零项消掉,再用第2行把第1行里第2列的非零项消掉,最终结果(具体中间过程略,直接给出消元后的结果):
[2003412140320121120043141234]
\left[\begin{array}{ccc|ccc} 2&0&0&\frac34&\frac12&\frac14\\0&\frac32&0&\frac12&1&\frac12\\0&0&\frac43&\frac14&\frac12&\frac34\end{array}\right]
2000230003443214121121412143
第四步:每行除以自己的主元,让左边变成单位矩阵
第1行除以2,第2行除以 32\frac3223,第3行除以 43\frac4334:
[I∣K−1]=[100381418010141214001181438]
[I \mid K^{-1}] = \left[\begin{array}{ccc|ccc} 1&0&0&\frac38&\frac14&\frac18\\0&1&0&\frac14&\frac12&\frac14\\0&0&1&\frac18&\frac14&\frac38\end{array}\right]
[I∣K−1]=100010001834181412141814183
所以:
K−1=14[321242123]
K^{-1} = \frac14\begin{bmatrix}3&2&1\\2&4&2\\1&2&3\end{bmatrix}
K−1=41321242123
关于 K−1K^{-1}K−1 的三个观察
- 对称性保留:KKK 是对称矩阵(沿主对角线翻折后不变),它的逆 K−1K^{-1}K−1 也是对称的。
- 稀疏变稠密:KKK 是"三对角矩阵"(只有主对角线及其紧邻两条线上有非零数),非常稀疏;但 K−1K^{-1}K−1 算出来却是处处非零的"稠密矩阵"。这正是为什么实际工程中很少真的去算逆矩阵——原本省内存的稀疏结构,求逆之后就完全消失了。
- 行列式 = 主元之积:三个主元 2, 32, 432,\ \frac32,\ \frac432, 23, 34 相乘:
2×32×43=4 2\times\frac32\times\frac43 = 4 2×23×34=4
这个 444 正是 KKK 的行列式。能看到 K−1K^{-1}K−1 公式里恰好有"除以 4",这不是巧合——逆矩阵的计算里必然要除以行列式,这就是为什么行列式一旦是 0,矩阵就没有逆(除以 0 是不允许的)。

通用流程图(mermaid)
下面用流程图总结高斯-若尔当法求逆矩阵的完整步骤:
五、求逆矩阵到底"值不值"?——计算成本分析
高斯-若尔当法求 A−1A^{-1}A−1,本质上是同时解 nnn 个方程组 Ax1=e1, Ax2=e2, …, Axn=enAx_1=e_1,\ Ax_2=e_2,\ \dots,\ Ax_n=e_nAx1=e1, Ax2=e2, …, Axn=en,所以总计算量是:
求 A−1 的乘除运算次数≈n3
\text{求 } A^{-1} \text{ 的乘除运算次数} \approx n^3
求 A−1 的乘除运算次数≈n3
而如果只是想解一个具体的方程 Ax=bAx=bAx=b(不需要求出 A−1A^{-1}A−1,直接消元到底再代入回去),计算量大约是:
直接解一个 Ax=b 的运算次数≈n33
\text{直接解一个 } Ax=b \text{ 的运算次数} \approx \frac{n^3}{3}
直接解一个 Ax=b 的运算次数≈3n3
结论很直接:如果你只是想解一两个具体的方程,直接消元解 xxx 比先算出整个 A−1A^{-1}A−1 再做 A−1bA^{-1}bA−1b 要快得多(大约快 3 倍)。这就是为什么书里反复强调——“在大多数实际问题里,我们根本不需要显式地算出 A−1A^{-1}A−1”。
| 求解方式 | 大致计算量 | 适用场景 |
|---|---|---|
| 直接消元解 Ax=bAx=bAx=b(一次) | n33\dfrac{n^3}{3}3n3 | 只需要解少量具体方程 |
| 先求出 A−1A^{-1}A−1 再算 A−1bA^{-1}bA−1b | n3n^3n3(求逆本身)+ 之后每次只需 n2n^2n2 | 要对很多不同的 bbb 反复求解 |
六、三角矩阵的逆——保持"形状不变"的好性质
上三角矩阵的逆仍是上三角矩阵
如果 AAA 是可逆的上三角矩阵(对角线及其右上方有数字,左下方全是 000),那么 A−1A^{-1}A−1 也一定是上三角矩阵。
直观理解:求 A−1A^{-1}A−1 的第 jjj 列,就是在解 Axj=ejAx_j = e_jAxj=ej,这里 eje_jej 这个单位向量除了第 jjj 个位置是 111,其余都是 000,特别是它后面(第 j+1j+1j+1 到第 nnn 个)位置全是 000。因为 AAA 是上三角的,往回代入(back substitution)求解时,这些后面的 000 会一直保持是 000,不会被"激活"成非零数。所以算出来的 xjx_jxj 这一列,后面 n−jn-jn−j 个位置必然也是 000——拼起来正好就是上三角的形状。
下三角矩阵(对角线全是1)的逆,仍是下三角(对角线全是1)
设:
L=[100310451]
L = \begin{bmatrix}1&0&0\\3&1&0\\4&5&1\end{bmatrix}
L=134015001
通过 Gauss-Jordan 消元(具体是用消元矩阵 E21,E31,E32E_{21},E_{31},E_{32}E21,E31,E32 依次把 LLL 化成 III),可以算出:
L−1=[100−31011−51]
L^{-1} = \begin{bmatrix}1&0&0\\-3&1&0\\11&-5&1\end{bmatrix}
L−1=1−31101−5001
注意这里出现了一个"奇怪"的数字 111111,它其实来自 3×5−4=113\times5-4=113×5−4=11 这样的交叉计算——这正是上面提到的"二次影响"问题在逆矩阵里的体现,提醒我们:逆矩阵的元素不是简单地把原矩阵元素取负号就行,中间往往有非线性的组合。

七、完整可运行的 C++ 代码:高斯-若尔当法求逆矩阵
下面这份代码完整实现了高斯-若尔当消元法,输入一个 n×nn\times nn×n 矩阵,输出它的逆矩阵(如果存在)。代码不使用万能头文件,所有用到的标准库都单独列出,并且每一步都配有详细中文注释。
#include <iostream> // 用于标准输入输出 std::cin, std::cout
#include <vector> // 用于 std::vector 动态数组,存放矩阵
#include <cmath> // 用于 std::fabs 取绝对值(判断主元是否接近0)
#include <iomanip> // 用于 std::setprecision 控制输出的小数位数
// 使用 using 简化类型名,Matrix 就是"二维 vector",表示一个矩阵
using Matrix = std::vector<std::vector<double>>;
// ---------------------------------------------------------
// 函数:打印矩阵,方便调试和查看中间结果
// ---------------------------------------------------------
void printMatrix(const Matrix& M) {
// 设置输出精度为4位小数,固定小数格式
std::cout << std::fixed << std::setprecision(4);
for (const auto& row : M) { // 遍历矩阵的每一行
for (double val : row) { // 遍历这一行的每一个数
std::cout << val << "\t"; // 用 tab 分隔,方便对齐
}
std::cout << "\n"; // 一行结束后换行
}
std::cout << "\n";
}
// ---------------------------------------------------------
// 函数:高斯-若尔当消元法求逆矩阵
// 输入:n x n 的矩阵 A
// 输出:如果 A 可逆,返回 true,并把结果存入 invA;
// 如果 A 不可逆(出现全零主元列),返回 false。
// ---------------------------------------------------------
bool gaussJordanInverse(Matrix A, Matrix& invA) {
int n = static_cast<int>(A.size()); // 矩阵的阶数(行数=列数)
// ---------- 第一步:构造增广矩阵 [A | I] ----------
// aug 是一个 n 行、2n 列的矩阵:左边 n 列放 A,右边 n 列放单位矩阵 I
Matrix aug(n, std::vector<double>(2 * n, 0.0));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
aug[i][j] = A[i][j]; // 左半部分:复制原矩阵 A
}
aug[i][n + i] = 1.0; // 右半部分:单位矩阵,只有对角线是1
}
// ---------- 第二步:对每一列做消元,化简到 [I | A^{-1}] ----------
for (int col = 0; col < n; ++col) {
// (a) 选主元:在当前列里,从第 col 行往下找绝对值最大的数,
// 这样做("部分主元消元")可以减少数值误差,也能在
// 当前行恰好是0时,通过换行避免除以0的问题。
int pivotRow = col;
double maxVal = std::fabs(aug[col][col]);
for (int r = col + 1; r < n; ++r) {
if (std::fabs(aug[r][col]) > maxVal) {
maxVal = std::fabs(aug[r][col]);
pivotRow = r;
}
}
// (b) 如果最大主元都几乎是 0,说明这一列消不出非零主元,
// 矩阵不可逆,直接返回 false。
const double EPS = 1e-9; // 数值容差,比这个还小就当作0
if (maxVal < EPS) {
return false; // A 不可逆
}
// (c) 如果最大主元不在当前行,就交换两行(对应"行交换"操作)
if (pivotRow != col) {
std::swap(aug[pivotRow], aug[col]);
}
// (d) 把当前行除以主元,让主元变成1
// 这一步对应书里"每行除以自己的主元"
double pivotVal = aug[col][col];
for (int j = 0; j < 2 * n; ++j) {
aug[col][j] /= pivotVal;
}
// (e) 用当前这一行,把其他所有行在这一列的数消成0
// 这一步同时完成了"正向消元"(消下面)和
// "反向消元"(消上面),这正是 Gauss-Jordan 比
// 普通 Gauss 多做的部分。
for (int r = 0; r < n; ++r) {
if (r == col) continue; // 自己不用消自己
double factor = aug[r][col]; // 要消掉的那个数
if (std::fabs(factor) < EPS) continue; // 已经是0就跳过
for (int j = 0; j < 2 * n; ++j) {
aug[r][j] -= factor * aug[col][j];
}
}
}
// ---------- 第三步:把增广矩阵右半部分取出,就是 A^{-1} ----------
invA.assign(n, std::vector<double>(n, 0.0));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
invA[i][j] = aug[i][n + j];
}
}
return true; // 成功求出逆矩阵
}
// ---------------------------------------------------------
// 函数:矩阵乘法 C = A * B,用来验证 A * A^{-1} 是否等于单位矩阵
// ---------------------------------------------------------
Matrix matMul(const Matrix& A, const Matrix& B) {
int n = static_cast<int>(A.size()); // A 的行数
int m = static_cast<int>(B[0].size()); // B 的列数
int k = static_cast<int>(B.size()); // A 的列数 = B 的行数
Matrix C(n, std::vector<double>(m, 0.0));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
double sum = 0.0;
for (int t = 0; t < k; ++t) {
sum += A[i][t] * B[t][j]; // 点积法:第i行 点乘 第j列
}
C[i][j] = sum;
}
}
return C;
}
// ---------------------------------------------------------
// 主函数:用书中的矩阵 K 作为示例,验证求逆是否正确
// ---------------------------------------------------------
int main() {
// 书中的经典例子 K:三对角矩阵,对角线是2,旁边是-1
Matrix K = {
{ 2.0, -1.0, 0.0},
{-1.0, 2.0, -1.0},
{ 0.0, -1.0, 2.0}
};
std::cout << "原矩阵 K:\n";
printMatrix(K);
Matrix invK; // 用来存放求出的逆矩阵
// 调用高斯-若尔当消元函数求逆
bool ok = gaussJordanInverse(K, invK);
if (!ok) {
// 如果矩阵不可逆,打印提示并结束程序
std::cout << "矩阵 K 不可逆(出现了全零主元列)。\n";
return 0;
}
std::cout << "求得的逆矩阵 K^{-1}:\n";
printMatrix(invK);
// 验证:K 乘以 K^{-1} 应该等于单位矩阵 I
Matrix shouldBeI = matMul(K, invK);
std::cout << "验证 K * K^{-1}(应该等于单位矩阵):\n";
printMatrix(shouldBeI);
// 额外验证:构造一个不可逆的矩阵(行列式为0),测试程序能否正确识别
Matrix singularA = {
{1.0, 2.0},
{2.0, 4.0} // 第二行正好是第一行的2倍,ad-bc = 1*4-2*2 = 0
};
Matrix invSingular;
bool okSingular = gaussJordanInverse(singularA, invSingular);
std::cout << "测试不可逆矩阵 [[1,2],[2,4]]:\n";
if (!okSingular) {
std::cout << "程序正确识别:该矩阵不可逆。\n";
} else {
std::cout << "程序出错:本应不可逆,却返回了结果。\n";
}
return 0;
}
代码运行结果说明
这份代码会输出三部分内容:
- 原矩阵 KKK。
- 用高斯-若尔当法求出的 K−1K^{-1}K−1(应当与本文第四节手算的结果 14[321242123]\dfrac14\begin{bmatrix}3&2&1\\2&4&2\\1&2&3\end{bmatrix}41321242123 一致)。
- 验证 K×K−1K \times K^{-1}K×K−1 是否等于单位矩阵 III(对角线是1,其余是0,允许有极小的浮点误差)。
- 用矩阵 [1224]\begin{bmatrix}1&2\\2&4\end{bmatrix}[1224](行列式为0)测试程序是否能正确识别"不可逆"的情况。
代码中的部分主元选取(选当前列中绝对值最大的数作为主元)是工程实现中的标准做法,可以避免主元恰好是0或者数值非常小时带来的计算误差问题,这是对书中"理想化消元"的一个实用补充。
八、本节核心知识点总结
| 核心结论 | 一句话记忆 |
|---|---|
| 逆矩阵唯一性 | 左逆等于右逆,不会有两个不同的逆 |
| 可逆的充要条件 | nnn 个非零主元 ⇔\Leftrightarrow⇔ 行列式非0 ⇔\Leftrightarrow⇔ Ax=0Ax=0Ax=0 只有零解 |
| 乘积的逆 | (AB)−1=B−1A−1(AB)^{-1}=B^{-1}A^{-1}(AB)−1=B−1A−1,顺序要反过来 |
| 求逆的代价 | 比直接解一次方程贵 3 倍左右,所以能不求逆就不求 |
| 三角矩阵的逆 | 上三角的逆仍是上三角;下三角(对角线为1)的逆仍是下三角(对角线为1) |

1421

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



