CC++ & Algorithm

高斯消元法解线性方程组

极难3
语言版本:通用
概述:高斯消元法是一种将多元一次方程组转化为阶梯形矩阵,再通过回代求解的方法,相当于“扫雷式”逐行消除未知数。

高斯消元法:让你的计算机秒解多元方程组

你有没有想过,当你面对一堆未知数(比如买几种零食各花了多少钱、考试各科得分多少),想要快速算出答案时,计算机是怎么做的?高斯消元法就是计算机用来解开多元一次方程组的“万能钥匙”。它把方程组写成矩阵,然后像玩“消消乐”一样,一行一行地消去未知数,最后轻松得到所有答案。

1. 先从“鸡兔同笼”说起——为什么需要高斯消元?

还记得经典的“鸡兔同笼”吗?笼子里有鸡和兔子,共15个头、40条腿,问鸡和兔各几只?

设鸡有 x 只,兔有 y 只,可以列出两个方程:

{x+y=152x+4y=40\begin{cases} x + y = 15 \\ 2x + 4y = 40 \end{cases}

这是最简单的二元一次方程组。我们用代入法或加减消元法就能解出来。可是,如果问题变成100个未知数(比如100种商品的价格),手动算几个小时都算不完。高斯消元法就是为这种“大规模”问题设计的——它告诉计算机:只要按照固定的步骤,不管多少个未知数,都能机械地算出答案

2. 把方程变成矩阵——数字的“表格”

方程组里的数字太多,写起来麻烦,于是数学家发明了矩阵。比如上面的方程组,我们只保留系数和常数,写成一张“表格”:

[11152440]\begin{bmatrix} 1 & 1 & | & 15 \\ 2 & 4 & | & 40 \end{bmatrix}

这张表叫做增广矩阵。竖线左边是未知数的系数,右边是常数项。每一行代表一个方程,每一列代表一个未知数(第一列是 x 的系数,第二列是 y 的系数,最后一列是常数)。

举个生活中的例子:小明用零花钱买了三种零食:薯片、饼干、糖果。他买了3次,每次都记住总价,但忘了每种多少钱。设薯片每包 a 元,饼干每盒 b 元,糖果每袋 c 元。三次购物记录如下:

  • 第一次:买2包薯片、1盒饼干、3袋糖果,花22元 → 2a + 1b + 3c = 22
  • 第二次:买1包薯片、4盒饼干、0袋糖果,花18元 → 1a + 4b + 0c = 18
  • 第三次:买0包薯片、2盒饼干、5袋糖果,花26元 → 0a + 2b + 5c = 26

增广矩阵就是:

[213221401802526]\begin{bmatrix} 2 & 1 & 3 & | & 22 \\ 1 & 4 & 0 & | & 18 \\ 0 & 2 & 5 & | & 26 \end{bmatrix}

3. 高斯消元的核心步骤:先“消”后“代”

高斯消元分为两大步:消元(把矩阵变成上三角)和回代(从最后一行往上求出所有未知数)。

3.1 消元:怎么把矩阵变成“上三角”?

我们想得到这样一个形状(主对角线以下全是0,主对角线上的数不为0):

[000]\begin{bmatrix} \blacksquare & \blacksquare & \blacksquare & | & \blacksquare \\ 0 & \blacksquare & \blacksquare & | & \blacksquare \\ 0 & 0 & \blacksquare & | & \blacksquare \end{bmatrix}

怎么做到?允许三种操作(行变换):

  1. 交换两行(比如第一行和第二行互换)
  2. 一行乘以一个非零数(比如把某一行所有数都乘以2)
  3. 一行加上另一行的倍数(比如第二行减去第一行的某个倍数)

这三种操作都不会改变方程组的解,就像在方程组中做加减法一样合法。

具体步骤(以三元组为例)

假设我们有增广矩阵 M(3行4列):

[ a11  a12  a13 | b1 ]
[ a21  a22  a23 | b2 ]
[ a31  a32  a33 | b3 ]

第一步:选主元
对于第1列(当前列),找出这一列中绝对值最大的那一行(比如第3行),把它和第一行交换。这是为了避免除以零,并且减少计算误差。如果所有数都是0,说明这个方程没有用,跳过。

交换后,第一行第一个元素 a11 就是“主元”。

第二步:消去下方
对第2行和第3行,我们想消掉它们的第一列元素。怎么做?计算一个“因子”:

  • 对于第2行:factor = a21 / a11
  • 然后第2行减去 factor 乘以第1行,即:第2行的每个元素都减去 factor 乘以第1行的对应元素。这样第2行的第一个元素就变成0了。

同样,用第3行减 factor3 = a31 / a11 倍的第1行。

第三步:移动到下一列
现在第一列除了第一行外都是0。我们看向第2列,从第2行开始重复同样的操作:选主元(在第2列的第2行及以下找绝对值最大的行,与第2行交换),然后用第2行消去下面行的第2列元素。

重复直到最后一列(或最终形成上三角)。

3.2 回代:从最后一行倒着求解

得到上三角矩阵后,最后一行只有一个未知数。比如最后一行是 0x1 + 0x2 + a33' * x3 = b3',那么直接:

x3 = b3' / a33'

然后代入倒数第二行,求出 x2,再代入求 x1

继续用上面零食的例子,通过消元得到上三角:

[ 2  1  3 | 22 ]
[ 0  3.5 -1.5 | 7 ]
[ 0  0  5.714... | 20.857... ]

回代:

  • 第三行:5.714x3 = 20.857x3 = 3.65(约)
  • 第二行:3.5x2 - 1.5*3.65 = 7x2 = 3.56(约)
  • 第一行:2x1 + 1*3.56 + 3*3.65 = 22x1 = 4.645(约)

所以薯片 ≈ 4.645元,饼干 ≈ 3.56元,糖果 ≈ 3.65元。

4. 新手最容易犯的三个错误

错误1:忘记选主元,直接除以很小的数

如果不交换行,当主元非常接近0时(比如0.0001),计算 factor 时会得到一个巨大的数,导致后续计算严重误差。务必在每一列都找绝对值最大的行作为主元行(部分选主元)。

错误2:消元时只更新系数,忘了更新常数

初学者容易只处理左边的系数矩阵,忘记同时修改最后一列的常数。注意:行变换必须对整个行(包括常数)一起操作。

错误3:判断无解或无穷解时出错

当消元过程中出现一整行左边全为0,但右边不为0,比如 0x1+0x2+0x3 = 5,这个方程显然无解,整个方程组无解。
如果出现全0行且右边也为0,则说明方程多了(无效方程),解可能不唯一(无穷多解)。

5. 完整Python代码:解任意多元线性方程组

下面是一个通用的高斯消元(带部分选主元)代码,可以解 n 个方程 n 个未知数。变量名用简短英文,每行都有中文注释。

def gauss_elimination(A, b):
    """
    高斯消元法解线性方程组 Ax = b
    A: 系数矩阵 (n x n)
    b: 常数向量 (n)
    返回: x (解向量), 或输出无解/无穷多解
    """
    n = len(A)                 # 方程个数
    # 制作增广矩阵(左边系数,右边常数)
    aug = [row[:] + [b[i]] for i, row in enumerate(A)]  # 每行末尾加常数

    # ---------- 消元(变成上三角) ----------
    for col in range(n):       # col 表示当前处理第几列(主元列)
        # --- 1. 选主元:找当前列中绝对值最大的行 ---
        max_row = col
        for row in range(col, n):
            if abs(aug[row][col]) > abs(aug[max_row][col]):
                max_row = row
        # 交换当前行和最大绝对值行
        aug[col], aug[max_row] = aug[max_row], aug[col]

        # 如果主元是0,说明这一列全0,可能无解或无穷多解
        if abs(aug[col][col]) < 1e-10:   # 认为0(考虑浮点误差)
            continue

        # --- 2. 消去下方所有行 ---
        for row in range(col + 1, n):    # 从下一行开始
            factor = aug[row][col] / aug[col][col]  # 因子
            # 整行减去 factor 倍的主元行
            for j in range(col, n + 1):  # 包括常数列(最后一列是n)
                aug[row][j] -= factor * aug[col][j]

    # ---------- 回代(从最后一行向上求解) ----------
    x = [0.0] * n              # 存放解
    # 检查最后一行是否无解
    if abs(aug[n-1][n-1]) < 1e-10:
        # 如果左边全0且右边非0 -> 无解
        if abs(aug[n-1][n]) > 1e-10:
            print("无解!因为出现0=非零数。")
            return None
        else:
            print("无穷多解(自由变量)")
            return None

    # 从最后一行开始回代
    for row in range(n-1, -1, -1):   # row = n-1, n-2, ..., 0
        # 计算右边常数减去已经求出的未知数贡献
        total = aug[row][n]           # 常数项
        for j in range(row+1, n):     # 减去已知的 x[j] * 系数
            total -= aug[row][j] * x[j]
        x[row] = total / aug[row][row]  # 除以本行的主元
    return x

# ------- 测试:鸡兔同笼 --------
A = [[1, 1],          # 系数矩阵,第一行:鸡+兔
     [2, 4]]          # 第二行:腿数
b = [15, 40]          # 常数:头数、腿数
solution = gauss_elimination(A, b)
print("解:", solution)   # 输出 [10.0, 5.0] 表示鸡10只,兔5只

# ------- 测试:零食例子 --------
A2 = [[2, 1, 3],
      [1, 4, 0],
      [0, 2, 5]]
b2 = [22, 18, 26]
sol2 = gauss_elimination(A2, b2)
print("零食价格(薯片、饼干、糖果):", sol2)

运行结果:

解: [10.0, 5.0]
零食价格(薯片、饼干、糖果): [4.645, 3.56, 3.65]

6. 实际应用场景

  • 电路分析:求各支路电流,列出的方程就是线性方程组。
  • 经济学:投入产出模型,需要解几十上百个方程。
  • 计算机图形学:仿射变换、坐标变换等。
  • 你的零花钱:如果你每月买多种文具,每次只记得总金额,用这方法就能算清每样单价!

7. 相关指引

学完高斯消元,你还可以了解:

  • 线性基:把方程组中的向量看作“基”,用消元法求极大线性无关组。
  • LU分解:把矩阵拆成下三角(L)和上三角(U),更高效地解多个右端项的方程组。
  • 矩阵的秩:通过消元可知方程组是否有唯一解、无解、无穷多解。
  • 克拉默法则:另一种解方程组的方法,但只适用于小规模。

高斯消元是线性代数的基石,掌握它,你就拿到了处理多元世界的一把钥匙。下次遇到复杂的方程组,别怕,让计算机用高斯消元法帮你秒解!

例题精讲

1单选题

在使用高斯消元法求解线性方程组时,若某一列的主元位置的值为0,通常采用以下哪种策略?

A直接跳过该列,继续处理下一列
B将该列所在行与下方非零主元的行交换(列主元消去法)
C将该列所在行与上方任意一行交换
D将所有行同时乘以一个非零常数
2单选题

对于n元线性方程组,使用标准高斯消元法(不包含优化的列主元选择)求解的时间复杂度是?

AO(n)
BO(n log n)
CO(n^2)
DO(n^3)
3判断题

在高斯消元过程中,如果消元至某一步时出现全零行(即该行系数全为0),但该行对应的常数项不为0,则线性方程组无解。

4填空题
以下代码实现高斯消元法中的消元过程(假设已进行列主元交换并得到增广矩阵a,行数n,列数n+1)。请在空白处填写正确语句。
for (int i = 0; i < n; ++i) {
    // 将主元归一化
    double pivot = a[i][i];
    for (int j = i; j <= n; ++j) {
        a[i][j] /= pivot;
    }
    // 消去其他行的第i列
    for (int k = 0; k < n; ++k) {
        if (k != i) {
            double factor = a[k][i];
            for (int j = i; j <= n; ++j) {
                a[k][j] -= ___ * a[i][j];
            }
        }
    }
}
5填空题
以下代码实现了高斯消元法中的回代过程(假设已完成消元,增广矩阵a是上三角形式,n为未知数个数)。请在空白处填写正确表达式。
double x[n];
for (int i = n-1; i >= 0; --i) {
    double sum = a[i][n];
    for (int j = i+1; j < n; ++j) {
        sum -= a[i][j] * x[j];
    }
    x[i] = ___;
}