LU, QR, and Cholesky factorizations
See how LU, QR, and Cholesky each solve linear systems by exploiting structure, and why factoring is numerically preferable to inverting.
Inverting a matrix is expensive and numerically fragile. Instead, numerical linear algebra solves by factoring into simpler matrices, solving two easy triangular systems, and never forming [1].
Why triangular systems are cheap
A triangular system (lower triangular) is solved by forward substitution in time:
Upper triangular is solved by backward substitution, the same idea from bottom up. Factoring into two triangular matrices reduces the work of building to work of factoring once, then per right-hand side.
LU factorization
For a square (under mild conditions), Gaussian elimination produces
where is unit lower-triangular (1s on the diagonal) and is upper triangular. In practice, partial pivoting permutes rows: .
Algorithm. At step , eliminate column below the diagonal: for each row , set multiplier , subtract times row from row . The multipliers fill the lower triangle of .
Cost: to factor, to solve for each new .
Determinant: (the sign from the permutation).
QR factorization
For any (possibly rectangular) with :
where is orthogonal and is upper triangular. The economy (thin) form uses with .
Construction via Gram–Schmidt (previous lesson): the -th column of has for and above; the process exactly builds and simultaneously.
Solving via QR: multiply both sides by : , then backward-substitute. For least-squares problems (), QR avoids the formation of and is numerically more stable than the normal equations.
Cholesky factorization
For a symmetric positive definite matrix :
where is lower triangular with positive diagonal. Cholesky exists if and only if is PD (converse: if Cholesky succeeds, every leading minor is positive, confirming PD by Sylvester's criterion)[2].
Cost: , roughly half the cost of LU, exploiting symmetry.
Algorithm. Column by column, from to :
A negative value under the square root means is not PD.
Inverse: , but forming the inverse is usually unnecessary: solve (forward substitution) then (backward substitution).
Check it on arrays
import numpy as np
A = np.array([[4.0, 3.0, 2.0],
[3.0, 6.0, 1.0],
[2.0, 1.0, 5.0]]) # symmetric PD
b = np.array([1.0, 2.0, 3.0])
# Triangular solves (the cheap O(n^2) step every factorization reduces to):
def forward_sub(L, b): # L lower triangular
x = np.zeros_like(b)
for i in range(len(b)):
x[i] = (b[i] - L[i, :i] @ x[:i]) / L[i, i]
return x
def back_sub(U, b): # U upper triangular
x = np.zeros_like(b)
for i in reversed(range(len(b))):
x[i] = (b[i] - U[i, i + 1:] @ x[i + 1:]) / U[i, i]
return x
# LU without pivoting (A is PD here, so no row swaps are needed):
def lu(A):
n = A.shape[0]
L, U = np.eye(n), A.astype(float).copy()
for k in range(n):
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 L, U
L, U = lu(A)
print("LU reconstructs A:", np.allclose(L @ U, A))
x_lu = back_sub(U, forward_sub(L, b)) # solve Ly=b, then Ux=y
# QR: A = QR, so Ax=b becomes Rx = Q^T b
Q, R = np.linalg.qr(A)
x_qr = back_sub(R, Q.T @ b)
print("LU == QR solution:", np.allclose(x_lu, x_qr))
# Cholesky: A = L L^T, solve Ly=b then L^T x=y
L_chol = np.linalg.cholesky(A) # lower triangular
y = forward_sub(L_chol, b)
x_chol = back_sub(L_chol.T, y)
print("LU == Cholesky solution:", np.allclose(x_lu, x_chol))
# Never invert: compare condition number
print("cond(A):", np.linalg.cond(A)) # moderate -> solving is fineAll three paths give the same solution; the Cholesky factorization halves the arithmetic.
Exercises
Exercise 1. Solve the lower-triangular system by hand using forward substitution, with and .
Solution
Step 1. Forward substitution solves top to bottom. Row : , so .
Step 2. Row : , so and .
Step 3. Row : , so , giving . The solution is .
Exercise 2. Compute the Cholesky factor of by hand, and use it to confirm is positive definite.
Solution
Step 1. Apply the column formula. For : , and .
Step 2. For : . So .
Step 3. The square roots were both of strictly positive numbers ( and ), so Cholesky succeeds and is positive definite. Verify .
Exercise 3 (code). For a random symmetric positive definite , solve
three ways (LU via np.linalg.solve, QR, and Cholesky) and assert all
three solutions agree. Then confirm that the product of the squared Cholesky
diagonal entries equals .
Solution
import numpy as np
rng = np.random.default_rng(0)
M = rng.normal(size=(4, 4))
A = M @ M.T + 4 * np.eye(4) # symmetric positive definite
b = rng.normal(size=4)
x_lu = np.linalg.solve(A, b) # LU with pivoting under the hood
Q, R = np.linalg.qr(A)
x_qr = np.linalg.solve(R, Q.T @ b) # Rx = Q^T b
L = np.linalg.cholesky(A) # A = L L^T
y = np.linalg.solve(L, b)
x_chol = np.linalg.solve(L.T, y)
assert np.allclose(x_lu, x_qr)
assert np.allclose(x_lu, x_chol)
# det A = product of l_ii^2, since det A = det L * det L^T = (prod l_ii)^2.
assert np.isclose(np.prod(np.diag(L)**2), np.linalg.det(A))
print("solution:", np.round(x_lu, 6))The three factorizations land on the same solution, and the squared product of the Cholesky diagonal reproduces because and is triangular.
The next lesson uses QR and the SVD together to handle the case where has no exact solution.
References
- [1]Trefethen, L. N., & Bau, D. (1997). Numerical Linear Algebra. SIAM. LU, QR, and Cholesky factorizations and why factoring beats inverting (Lectures 7, 20, 23).↩
- [2]Golub, G. H., & Van Loan, C. F. (2013). Matrix Computations (4th ed.). Johns Hopkins University Press. The canonical reference for matrix factorization algorithms and their costs (doi:10.56021/9781421407944).↩