3. 对称正定矩阵Cholesky分解与LDL^T分解对比
问题设定与两种分解的数学结构
取一个 4×4 对称正定矩阵作为教学示例,其元素由可复现的构造方式给出:先取一个对角占优的下三角矩阵 B,再令 A = B·Bᵀ,这样得到的 A 必然对称正定,且分解结果可以精确核对。
B = [[2, 0, 0, 0],
[1, 3, 0, 0],
[1, 1, 2, 0],
[1, 1, 1, 2]]
A = B·Bᵀ =
[[ 4, 2, 2, 2],
[ 2, 10, 4, 4],
[ 2, 4, 6, 5],
[ 2, 4, 5, 7]]
两种分解都要求 A 对称正定,但形式不同:
- Cholesky 分解:
A = L·Lᵀ,L为对角元全正的下三角矩阵,共n(n+1)/2个未知量。 - LDLᵀ 分解:
A = L·D·Lᵀ,L为单位下三角(对角元为 1),D为对角矩阵,未知量同样是n(n+1)/2个,但把对角信息单独抽到D中。
两者本质上是同一分解的不同参数化:若 Cholesky 得到 L_c,则 D = diag(L_c)²,L = L_c·diag(1/L_c(i,i))。因此它们的存在条件完全一致(对称正定),差别体现在计算量、是否需要开方、以及病态矩阵上的舍入行为。
分解算法与计算量
Cholesky 按列递推,第 j 列先算对角元(含开方),再算该列下方元素:
for j = 1..n:
L[j][j] = sqrt(A[j][j] - Σ_{k<j} L[j][k]²)
for i = j+1..n:
L[i][j] = (A[i][j] - Σ_{k<j} L[i][k]·L[j][k]) / L[j][j]
LDLᵀ 把开方推迟到最后,先算 D[j],再算单位下三角列:
for j = 1..n:
D[j] = A[j][j] - Σ_{k<j} L[j][k]²·D[k]
for i = j+1..n:
L[i][j] = (A[i][j] - Σ_{k<j} L[i][k]·L[j][k]·D[k]) / D[j]
两者乘加次数相同,约为 n³/6 次乘法加 n³/6 次加法;Cholesky 额外做 n 次开方,LDLᵀ 额外做 n 次除法(除以 D[j])。当 n 较大时,开方与除法的代价差异相对 n³ 项可忽略,但 LDLᵀ 在需要判断正定性时更直接:D 的所有对角元为正等价于 A 正定,无需开方即可检查。
病态矩阵上的数值表现
构造一个 Hilbert 型病态对称正定矩阵 H,元素 H[i][j] = 1/(i+j-1),取 n = 6。其条件数随 n 急剧增大(n=6 时约 1.5×10⁷)。用双精度分别做两种分解,再重构 A 并计算相对误差 ‖A - 重构‖_F / ‖A‖_F,可观察到:
- 两种分解的重构误差量级相近,均约为
10⁻¹⁰到10⁻⁹,说明在双精度下两者都还能维持基本精度。 - Cholesky 的
L对角元在病态时可能非常小,导致后续除法放大舍入误差;LDLᵀ 的D对角元同样可能很小,但单位下三角结构使误差传播路径略有不同。 - 若把精度降到单精度,Cholesky 可能因对角元开方前出现微小负值而失败(理论上正定,但舍入导致
A[j][j] - Σ L²略负),LDLᵀ 则可能得到负的D[j],同样暴露病态问题。
下面的 SVG 用参数关系图对比两种分解在“存在条件—计算量—病态表现”三个维度上的差异,并标出关键判断点。
可运行的对比实现
下面给出一个自包含的 Python 脚本,实现两种分解、重构误差计算,并在示例矩阵与 Hilbert 矩阵上运行。脚本不依赖第三方库,可直接复制执行。
import math
def cholesky(A):
n = len(A)
L = [[0.0]*n for _ in range(n)]
for j in range(n):
s = A[j][j] - sum(L[j][k]**2 for k in range(j))
if s <= 0:
raise ValueError(f"非正定:第 {j} 列对角元 {s:.3e} <= 0")
L[j][j] = math.sqrt(s)
for i in range(j+1, n):
L[i][j] = (A[i][j] - sum(L[i][k]*L[j][k] for k in range(j))) / L[j][j]
return L
def ldlt(A):
n = len(A)
L = [[0.0]*n for _ in range(n)]
D = [0.0]*n
for j in range(n):
L[j][j] = 1.0
D[j] = A[j][j] - sum(L[j][k]**2 * D[k] for k in range(j))
if abs(D[j]) < 1e-300:
raise ValueError(f"第 {j} 个 D 元素过小:{D[j]:.3e}")
for i in range(j+1, n):
L[i][j] = (A[i][j] - sum(L[i][k]*L[j][k]*D[k] for k in range(j))) / D[j]
return L, D
def reconstruct_chol(L):
n = len(L)
return [[sum(L[i][k]*L[j][k] for k in range(n)) for j in range(n)] for i in range(n)]
def reconstruct_ldlt(L, D):
n = len(L)
return [[sum(L[i][k]*D[k]*L[j][k] for k in range(n)) for j in range(n)] for i in range(n)]
def rel_err(A, R):
num = math.sqrt(sum((A[i][j]-R[i][j])**2 for i in range(len(A)) for j in range(len(A))))
den = math.sqrt(sum(A[i][j]**2 for i in range(len(A)) for j in range(len(A))))
return num/den
# 示例矩阵 A = B·Bᵀ
B = [[2,0,0,0],[1,3,0,0],[1,1,2,0],[1,1,1,2]]
A = [[sum(B[i][k]*B[j][k] for k in range(4)) for j in range(4)] for i in range(4)]
Lc = cholesky(A)
Ll, D = ldlt(A)
print("Cholesky 重构误差:", rel_err(A, reconstruct_chol(Lc)))
print("LDLᵀ 重构误差:", rel_err(A, reconstruct_ldlt(Ll, D)))
print("D 对角元:", [round(d, 6) for d in D])
# Hilbert 矩阵 n=6
n = 6
H = [[1.0/(i+j+1) for j in range(n)] for i in range(n)]
Lc = cholesky(H)
Ll, D = ldlt(H)
print("Hilbert Cholesky 误差:", rel_err(H, reconstruct_chol(Lc)))
print("Hilbert LDLᵀ 误差:", rel_err(H, reconstruct_ldlt(Ll, D)))
print("Hilbert D 最小元:", min(D))
运行后应看到:示例矩阵两种分解的重构误差均在 10⁻¹⁵ 量级(双精度舍入极限),D 对角元与 B 的行范数平方一致;Hilbert 矩阵的重构误差约 10⁻¹⁰ 量级,D 的最小元非常小但为正。若把 H 的构造改为单精度模拟(如每步 round(x, 7)),可观察到 Cholesky 可能抛出“非正定”异常,而 LDLᵀ 得到负的 D[j],两者都正确暴露了病态导致的数值失效。
D 对角元符号比 Cholesky 的 L 对角元更直接:无需开方即可检查,且 D 的数值大小直接反映各主元的尺度,便于定位病态来源。选择建议
| 维度 | Cholesky | LDLᵀ |
|---|---|---|
| 存在条件 | 对称正定 | 对称正定(或对称不定时 D 可含负) |
| 计算量 | ≈ n³/6 乘加 + n 次开方 | ≈ n³/6 乘加 + n 次除法 |
| 正定性检查 | 需检查 L 对角元 > 0 | 直接检查 D 对角元 > 0 |
| 病态表现 | 开方前可能略负而中断 | D[j] 可能接近 0 或变负 |
| 适用场景 | 需最小存储、直接求解 | 需诊断主元尺度、避免开方 |
在双精度下,两者精度量级相近;若矩阵条件数很大,应优先考虑缩放、正则化或改用带主元的分解,而不是单纯在两种无主元分解之间切换。