1. 线性方程组直接法对比:高斯消元与LU分解

问题设定与两种直接法的实现

本节针对同一个稠密线性方程组 Ax = b,分别用高斯消元(Gauss Elimination, GE)与 LU 分解(Doolittle,带部分主元)求解,并对比计算量、数值稳定性与主元策略的影响,最后用残差 ‖b − Ax̂‖∞ 验证解的正确性。

为便于复现,取一个 4×4 的示例矩阵(教学示例数据,非实测):

A = [[ 4, -2,  1,  3],
     [ 8, -3,  2,  7],
     [-4,  1, -3,  1],
     [ 6, -5,  4, 12]]
b = [ 7, 17, -9, 21]

两种方法的核心差异在于:高斯消元把右端项 b 与系数矩阵同步消元,一次性得到解;LU 分解只对 A 做分解得到 L、U 与置换 P,再通过前代/回代处理 b。因此同一 A 面对多个右端项时,LU 分解只需分解一次。

关键实现(Python,可直接运行)

import numpy as np

def gauss_elimination(A, b):
    A = A.astype(float).copy()
    b = b.astype(float).copy()
    n = len(b)
    swaps = 0
    for k in range(n - 1):
        # 部分主元:选列主元绝对值最大者
        p = k + np.argmax(np.abs(A[k:, k]))
        if p != k:
            A[[k, p]] = A[[p, k]]
            b[[k, p]] = b[[p, k]]
            swaps += 1
        if abs(A[k, k]) < 1e-14:
            raise ValueError("矩阵奇异或接近奇异")
        for i in range(k + 1, n):
            m = A[i, k] / A[k, k]
            A[i, k:] -= m * A[k, k:]
            b[i] -= m * b[k]
    # 回代
    x = np.zeros(n)
    for i in range(n - 1, -1, -1):
        x[i] = (b[i] - A[i, i+1:] @ x[i+1:]) / A[i, i]
    return x, swaps

def lu_decompose(A):
    A = A.astype(float).copy()
    n = A.shape[0]
    L = np.eye(n)
    U = A.copy()
    P = np.eye(n)
    swaps = 0
    for k in range(n - 1):
        p = k + np.argmax(np.abs(U[k:, k]))
        if p != k:
            U[[k, p]] = U[[p, k]]
            P[[k, p]] = P[[p, k]]
            L[[k, p], :k] = L[[p, k], :k]   # 只交换已生成的乘子
            swaps += 1
        if abs(U[k, k]) < 1e-14:
            raise ValueError("矩阵奇异或接近奇异")
        for i in range(k + 1, n):
            L[i, k] = U[i, k] / U[k, k]
            U[i, k:] -= L[i, k] * U[k, k:]
    return P, L, U, swaps

def lu_solve(P, L, U, b):
    b = P @ b.astype(float)
    n = len(b)
    y = np.zeros(n)
    for i in range(n):
        y[i] = b[i] - L[i, :i] @ y[:i]
    x = np.zeros(n)
    for i in range(n - 1, -1, -1):
        x[i] = (y[i] - U[i, i+1:] @ x[i+1:]) / U[i, i]
    return x

A = np.array([[ 4, -2,  1,  3],
              [ 8, -3,  2,  7],
              [-4,  1, -3,  1],
              [ 6, -5,  4, 12]], dtype=float)
b = np.array([7, 17, -9, 21], dtype=float)

x_ge, sw_ge = gauss_elimination(A, b)
P, L, U, sw_lu = lu_decompose(A)
x_lu = lu_solve(P, L, U, b)

print("GE 解:", x_ge, " 交换次数:", sw_ge)
print("LU 解:", x_lu, " 交换次数:", sw_lu)
print("GE 残差:", np.linalg.norm(b - A @ x_ge, np.inf))
print("LU 残差:", np.linalg.norm(b - A @ x_lu, np.inf))

预期输出(示例数据可直接推出):两种方法给出同一解 x ≈ [1, 2, -1, 1],残差均在 1e-14 量级(浮点舍入范围内)。若残差明显偏大,说明主元选择或回代实现有误。

计算量与主元策略对比

对比项高斯消元(带部分主元)LU 分解(Doolittle + 部分主元)
分解阶段乘除次数≈ n³/3≈ n³/3
右端项处理消元时同步更新 b,O(n²)前代 + 回代,O(n²)
多右端项场景每个 b 都要重新消元分解一次,复用 L、U、P
存储就地覆盖 A,额外 O(n)需保存 L、U、P,约 2n²
主元策略列主元交换行列主元交换行,同时交换 L 已生成部分
数值稳定性部分主元抑制小主元放大误差同左;L 乘子满足 |l_ik| ≤ 1
稠密方程组 A x = b 高斯消元(GE) ① 选列主元,交换行(A 与 b 同步) ② 消元:A 与 b 一起更新 ③ 回代得 x 乘除 ≈ n³/3 + n² 每个 b 都要重做一遍 LU 分解(Doolittle) ① 选列主元,交换行(U 与 P) ② 生成 L 乘子,更新 U ③ 前代 L y = P b ④ 回代 U x = y 分解 ≈ n³/3,每个 b 仅 O(n²) 解 x̂ 残差 ‖b−Ax̂‖∞ 部分主元策略 选 |a_ik| 最大者作主元 抑制小主元放大误差 LU 中 |l_ik| ≤ 1 结果判读 两法解应一致 残差 ≈ 1e-14 量级 残差偏大 → 查主元/回代 多右端项优先 LU

结果判读与选型建议

  • 解一致性:GE 与 LU 得到的 x̂ 应在浮点舍入范围内一致;若差异明显,优先检查 LU 中交换行时是否同步交换了 L 已生成的乘子。
  • 残差验证:用 ‖b − A x̂‖∞ 判断,示例数据下应接近机器精度;残差大通常意味着主元过小导致放大误差,或回代索引写错。
  • 计算量:单右端项时两者同阶(≈ n³/3);多右端项时 LU 分解一次即可复用,优势随右端项数量线性放大。
  • 稳定性:部分主元使乘子 |l_ik| ≤ 1,避免小主元放大误差;对病态矩阵仍需结合条件数评估。
若矩阵规模较大且右端项固定,可先做 LU 分解并缓存 P、L、U,后续求解只做前代与回代,避免重复 O(n³) 分解。