Show code
import numpy as np
print("✓ Libraries imported successfully")✓ Libraries imported successfully
Chao Ma
October 7, 2025
My lecture notes | Exercises notebook
Gilbert Strang’s fourth lecture introduces one of the most important matrix factorizations: LU decomposition, which factors any invertible matrix \(A\) into the product of a Lower triangular matrix and an Upper triangular matrix. This factorization is the foundation of efficient numerical linear algebra.
Goal: Factor any invertible matrix \(A\) as the product of: - \(L\) = Lower triangular matrix (with 1’s on diagonal) - \(U\) = Upper triangular matrix (the result of elimination)
\[ A = LU \]
Efficient solving: \(Ax = b\) becomes two simpler triangular solves:
Step 1 - Forward substitution: Solve \(Lc = b\) for \(c\)
Step 2 - Back substitution: Solve \(Ux = c\) for \(x\)
How this works:
Since \(A = LU\), we have \(Ax = LUx = b\). Let \(Ux = c\), then: \[ LUx = Lc = b \]
Forward substitution (solving \(Lc = b\)):
Since \(L\) is lower triangular with 1’s on the diagonal, we can solve for \(c\) step by step: \[ \begin{aligned} c_1 &= b_1 \\ c_2 &= b_2 - m_{21}c_1 \\ c_3 &= b_3 - m_{31}c_1 - m_{32}c_2 \\ &\vdots \end{aligned} \]
Each \(c_i\) depends only on previously computed values, so we solve forward from \(c_1\) to \(c_n\).
Back substitution (solving \(Ux = c\)):
Since \(U\) is upper triangular, we solve backward from \(x_n\) to \(x_1\): \[ \begin{aligned} x_n &= \frac{c_n}{u_{nn}} \\ x_{n-1} &= \frac{c_{n-1} - u_{n-1,n}x_n}{u_{n-1,n-1}} \\ &\vdots \end{aligned} \]
Result: We’ve solved \(Ax = b\) without ever explicitly computing \(A^{-1}\)!
Reusable factorization: When \(A\) is fixed but \(b\) changes, we can reuse \(L\) and \(U\)
Foundation of numerical computing: Used in MATLAB, NumPy, and all scientific computing libraries
Starting with \(A\), we apply elimination matrices \(E_{21}, E_{31}, E_{32}, \ldots\) to get upper triangular \(U\):
\[ E_{32} E_{31} E_{21} A = U \]
Example (3×3 case):
\[ A = \begin{bmatrix} 2 & 1 & 1 \\ 4 & -6 & 0 \\ -2 & 7 & 2 \end{bmatrix} \]
Step 1: Eliminate below first pivot (rows 2 and 3)
\[ E_{21} = \begin{bmatrix} 1 & 0 & 0 \\ -2 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}, \quad E_{31} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 1 & 0 & 1 \end{bmatrix} \]
Step 2: Eliminate below second pivot (row 3)
\[ E_{32} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & -1 & 1 \end{bmatrix} \]
An elimination matrix \(E_{ij}\) eliminates the entry at position \((i,j)\) by subtracting a multiple of row \(j\) from row \(i\).
General form: \[ E_{ij} = I - m_{ij} \mathbf{e}_i \mathbf{e}_j^T \]
where: - \(m_{ij}\) = multiplier = \(\frac{A_{ij}}{\text{pivot at } (j,j)}\) - \(\mathbf{e}_i\) = \(i\)-th standard basis vector - The \((i,j)\) entry of \(E_{ij}\) is \(-m_{ij}\)
Key properties: 1. Lower triangular (operates below diagonal) 2. Determinant = 1 (doesn’t change volume) 3. Easy to invert: \(E_{ij}^{-1} = I + m_{ij} \mathbf{e}_i \mathbf{e}_j^T\) (just flip the sign!)
From elimination, we have:
\[ E_{32} E_{31} E_{21} A = U \]
Multiply both sides by the inverses (in reverse order):
\[ A = E_{21}^{-1} E_{31}^{-1} E_{32}^{-1} U = LU \]
where: \[ L = E_{21}^{-1} E_{31}^{-1} E_{32}^{-1} \]
When elimination matrices are multiplied in the right order, their inverses combine beautifully:
\[ L = \begin{bmatrix} 1 & 0 & 0 & \cdots \\ m_{21} & 1 & 0 & \cdots \\ m_{31} & m_{32} & 1 & \cdots \\ \vdots & \vdots & \ddots & \ddots \end{bmatrix} \]
The multipliers \(m_{ij}\) (used during elimination) directly fill in the entries of \(L\) below the diagonal!
No extra computation needed — just save the multipliers as you eliminate.
For an \(n \times n\) matrix:
| Step | Operations | Order |
|---|---|---|
| Elimination (find U) | \(\frac{n^3}{3} + O(n^2)\) | \(O(n^3)\) |
| Forward substitution \((Lc = b)\) | \(\frac{n^2}{2}\) | \(O(n^2)\) |
| Back substitution \((Ux = c)\) | \(\frac{n^2}{2}\) | \(O(n^2)\) |
At step \(k\), we update an \((n-k) \times (n-k)\) submatrix:
\[ \text{Total operations} = \sum_{k=1}^{n-1} (n-k)^2 \approx \int_0^n x^2 \, dx = \frac{n^3}{3} \]
Single solve: \(Ax = b\) costs \(O(n^3)\) either way
Multiple solves: If solving \(Ax = b_1, Ax = b_2, \ldots, Ax = b_m\): - Without LU: \(m \times O(n^3)\) - With LU: \(O(n^3)\) (once) + \(m \times O(n^2)\) ✅
Huge savings when \(A\) is fixed but \(b\) changes!
Let’s practice LU decomposition with concrete examples.
✓ Libraries imported successfully
Compute the LU decomposition of \(A = \begin{bmatrix} 2 & 3 \\ 4 & 7 \end{bmatrix}\) by hand.
Steps: 1. Perform elimination to get \(U\) 2. Record the multiplier \(m_{21}\) to build \(L\) 3. Verify \(A = LU\)
from IPython.display import display, Markdown, Latex
# Original matrix
A = np.array([[2, 3],
[4, 7]])
display(Markdown("**Original matrix A:**"))
display(Latex(r"$$A = \begin{bmatrix} 2 & 3 \\ 4 & 7 \end{bmatrix}$$"))
# Compute multiplier m21
m21 = 4/2 # row2[0] / row1[0]
display(Markdown("<hr />"))
display(Markdown("**Step 1: Compute multiplier**"))
display(Latex(f"$$m_{{21}} = \\frac{{4}}{{2}} = {m21}$$"))
# Build L matrix
L = np.array([[1, 0],
[m21, 1]])
display(Markdown("<hr />"))
display(Markdown("**Step 2: Build L matrix**"))
display(Latex(r"$$L = \begin{bmatrix} 1 & 0 \\ m_{21} & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 \\ 2 & 1 \end{bmatrix}$$"))
# Build U matrix (result after elimination)
# After: row2 = row2 - m21*row1
# [2, 3] [2, 3]
# [4, 7] --> [0, 1] (because 7 - 2*3 = 1)
U = np.array([[2, 3],
[0, 1]])
display(Markdown("<hr />"))
display(Markdown("**Step 3: Build U matrix (after elimination)**"))
display(Markdown("Row 2 → Row 2 - 2 × Row 1"))
display(Latex(r"$$U = \begin{bmatrix} 2 & 3 \\ 0 & 1 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("**Verification: $A = LU$**"))
display(Latex(r"$$LU = \begin{bmatrix} 1 & 0 \\ 2 & 1 \end{bmatrix} \begin{bmatrix} 2 & 3 \\ 0 & 1 \end{bmatrix} = \begin{bmatrix} 2 & 3 \\ 4 & 7 \end{bmatrix} = A \quad \checkmark$$"))Original matrix A:
\[A = \begin{bmatrix} 2 & 3 \\ 4 & 7 \end{bmatrix}\]
Step 1: Compute multiplier
\[m_{21} = \frac{4}{2} = 2.0\]
Step 2: Build L matrix
\[L = \begin{bmatrix} 1 & 0 \\ m_{21} & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 \\ 2 & 1 \end{bmatrix}\]
Step 3: Build U matrix (after elimination)
Row 2 → Row 2 - 2 × Row 1
\[U = \begin{bmatrix} 2 & 3 \\ 0 & 1 \end{bmatrix}\]
Verification: \(A = LU\)
\[LU = \begin{bmatrix} 1 & 0 \\ 2 & 1 \end{bmatrix} \begin{bmatrix} 2 & 3 \\ 0 & 1 \end{bmatrix} = \begin{bmatrix} 2 & 3 \\ 4 & 7 \end{bmatrix} = A \quad \checkmark\]
Key observation: The multiplier \(m_{21} = 2\) goes directly into position \((2,1)\) of \(L\)!
Perform LU decomposition on:
\[ A = \begin{bmatrix} 2 & 1 & 1 \\ 4 & -6 & 0 \\ -2 & 7 & 2 \end{bmatrix} \]
Goal: Find \(L\) and \(U\) such that \(A = LU\)
from IPython.display import display, Markdown, Latex
A = np.array([[2, 1, 1],
[4, -6, 0],
[-2, 7, 2]])
display(Markdown("**Original matrix A:**"))
display(Latex(r"$$A = \begin{bmatrix} 2 & 1 & 1 \\ 4 & -6 & 0 \\ -2 & 7 & 2 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("### Step 1: Eliminate column 1"))
# Calculate multipliers for column 1
m21 = A[1, 0] / A[0, 0] # 4/2 = 2
m31 = A[2, 0] / A[0, 0] # -2/2 = -1
display(Markdown("**Multipliers:**"))
display(Latex(f"$$m_{{21}} = \\frac{{4}}{{2}} = {m21}, \\quad m_{{31}} = \\frac{{-2}}{{2}} = {m31}$$"))
# Create A1 after first elimination
A1 = A.copy().astype(float)
A1[1] = A1[1] - m21 * A1[0] # row2 - 2*row1
A1[2] = A1[2] - m31 * A1[0] # row3 - (-1)*row1
display(Markdown("**After eliminating column 1:**"))
display(Markdown("- Row 2 → Row 2 - 2 × Row 1"))
display(Markdown("- Row 3 → Row 3 - (-1) × Row 1"))
display(Latex(r"$$A^{(1)} = \begin{bmatrix} 2 & 1 & 1 \\ 0 & -8 & -2 \\ 0 & 8 & 3 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("### Step 2: Eliminate column 2"))
# Calculate multiplier for column 2
m32 = A1[2, 1] / A1[1, 1] # 8/(-8) = -1
display(Markdown("**Multiplier:**"))
display(Latex(f"$$m_{{32}} = \\frac{{8}}{{-8}} = {m32}$$"))
# Create U (final upper triangular)
U = A1.copy()
U[2] = U[2] - m32 * U[1] # row3 - (-1)*row2
display(Markdown("**After eliminating column 2:**"))
display(Markdown("- Row 3 → Row 3 - (-1) × Row 2"))
display(Latex(r"$$U = \begin{bmatrix} 2 & 1 & 1 \\ 0 & -8 & -2 \\ 0 & 0 & 1 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("### Build L from multipliers"))
# Build L from multipliers
L = np.array([[1, 0, 0],
[m21, 1, 0],
[m31, m32, 1]])
display(Markdown("The multipliers directly fill in $L$:"))
display(Latex(r"$$L = \begin{bmatrix} 1 & 0 & 0 \\ m_{21} & 1 & 0 \\ m_{31} & m_{32} & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 2 & 1 & 0 \\ -1 & -1 & 1 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("### Verification: $A = LU$"))
display(Latex(r"$$LU = \begin{bmatrix} 1 & 0 & 0 \\ 2 & 1 & 0 \\ -1 & -1 & 1 \end{bmatrix} \begin{bmatrix} 2 & 1 & 1 \\ 0 & -8 & -2 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 2 & 1 & 1 \\ 4 & -6 & 0 \\ -2 & 7 & 2 \end{bmatrix} = A \quad \checkmark$$"))Original matrix A:
\[A = \begin{bmatrix} 2 & 1 & 1 \\ 4 & -6 & 0 \\ -2 & 7 & 2 \end{bmatrix}\]
Multipliers:
\[m_{21} = \frac{4}{2} = 2.0, \quad m_{31} = \frac{-2}{2} = -1.0\]
After eliminating column 1:
\[A^{(1)} = \begin{bmatrix} 2 & 1 & 1 \\ 0 & -8 & -2 \\ 0 & 8 & 3 \end{bmatrix}\]
Multiplier:
\[m_{32} = \frac{8}{-8} = -1.0\]
After eliminating column 2:
\[U = \begin{bmatrix} 2 & 1 & 1 \\ 0 & -8 & -2 \\ 0 & 0 & 1 \end{bmatrix}\]
The multipliers directly fill in \(L\):
\[L = \begin{bmatrix} 1 & 0 & 0 \\ m_{21} & 1 & 0 \\ m_{31} & m_{32} & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 2 & 1 & 0 \\ -1 & -1 & 1 \end{bmatrix}\]
\[LU = \begin{bmatrix} 1 & 0 & 0 \\ 2 & 1 & 0 \\ -1 & -1 & 1 \end{bmatrix} \begin{bmatrix} 2 & 1 & 1 \\ 0 & -8 & -2 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 2 & 1 & 1 \\ 4 & -6 & 0 \\ -2 & 7 & 2 \end{bmatrix} = A \quad \checkmark\]
Key observation: All three multipliers \((m_{21}, m_{31}, m_{32})\) go directly into their corresponding positions in \(L\):
\[ L = \begin{bmatrix} 1 & 0 & 0 \\ m_{21} & 1 & 0 \\ m_{31} & m_{32} & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 2 & 1 & 0 \\ -1 & -1 & 1 \end{bmatrix} \]
Note: In practice, numerical libraries like SciPy provide scipy.linalg.lu() which computes LU decomposition efficiently and includes automatic row permutation (pivoting) for numerical stability.
from scipy.linalg import lu
from IPython.display import display, Markdown, Latex
A = np.array([[2, 1, 1],
[4, -6, 0],
[-2, 7, 2]], dtype=float)
# SciPy returns P, L, U where PA = LU (P is permutation matrix)
P, L_scipy, U_scipy = lu(A)
display(Markdown("**SciPy's LU decomposition:**"))
display(Markdown("SciPy returns $P$, $L$, $U$ where $PA = LU$ ($P$ is a permutation matrix)"))
display(Markdown("<hr />"))
display(Markdown("**Permutation matrix P:**"))
display(Latex(r"$$P = \begin{bmatrix} 0 & 1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix}$$"))
display(Markdown("(This swaps rows 1 and 2 for numerical stability)"))
display(Markdown("<hr />"))
display(Markdown("**Lower triangular L:**"))
display(Latex(r"$$L = \begin{bmatrix} 1 & 0 & 0 \\ 0.5 & 1 & 0 \\ -0.5 & 1 & 1 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("**Upper triangular U:**"))
display(Latex(r"$$U = \begin{bmatrix} 4 & -6 & 0 \\ 0 & 4 & 1 \\ 0 & 0 & 1 \end{bmatrix}$$"))
display(Markdown("<hr />"))
display(Markdown("**Verification: $PA = LU$**"))
# Note: If P = I (identity), then our manual L and U should match
if np.allclose(P, np.eye(3)):
display(Markdown("✓ No row swaps needed! Our manual $L$ and $U$ match SciPy."))
else:
display(Markdown("⚠ **Row swaps were performed** (pivot strategy for numerical stability)."))
display(Markdown("SciPy chose the largest pivot to minimize rounding errors."))
display(Markdown("Our manual decomposition is valid but uses a different pivot order."))SciPy’s LU decomposition:
SciPy returns \(P\), \(L\), \(U\) where \(PA = LU\) (\(P\) is a permutation matrix)
Permutation matrix P:
\[P = \begin{bmatrix} 0 & 1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix}\]
(This swaps rows 1 and 2 for numerical stability)
Lower triangular L:
\[L = \begin{bmatrix} 1 & 0 & 0 \\ 0.5 & 1 & 0 \\ -0.5 & 1 & 1 \end{bmatrix}\]
Upper triangular U:
\[U = \begin{bmatrix} 4 & -6 & 0 \\ 0 & 4 & 1 \\ 0 & 0 & 1 \end{bmatrix}\]
Verification: \(PA = LU\)
⚠ Row swaps were performed (pivot strategy for numerical stability).
SciPy chose the largest pivot to minimize rounding errors.
Our manual decomposition is valid but uses a different pivot order.