Why LU Decomposition Matters
Imagine a circuit model whose connections and component values stay fixed while the input voltages change. Each experiment produces a new right-hand side in $Ax=b$, but the coefficient matrix $A$ stays the same. Repeating all of Gaussian elimination for every experiment wastes work. LU decomposition saves the elimination work in two triangular matrices, so each later solve needs only two substitution passes.
The letters describe the shapes: $L$ is lower triangular, and $U$ is upper triangular. Row swaps make the method work when a diagonal candidate is zero or inconveniently small. We will record those swaps in a permutation $P$ and use the convention $PA=LU$. The same factors will solve two different systems, and independent residuals will check both the factorization and the answers.
By the end, you should be able to explain what a stored elimination multiplier means, trace a pivot that moves earlier multipliers, implement a reusable factor-and-solve interface in three languages, and distinguish numerical rejection from an exact proof of singularity.
LU decomposition turns repeated solves with one matrix into a reusable factorization: first compute $PA=LU$, then solve $Ly=Pb$ and $Ux=y$ for each new $b$. The expensive elimination happens once; the triangular factors preserve the work needed for later right-hand sides.
Core Concepts and Terminology
The implementation accepts a nonempty finite square matrix $A\in\mathbb{R}^{n\times n}$ and a relative pivot tolerance $0\lt\tau\lt1$. On success it returns a unit lower triangular matrix $L$, an upper triangular matrix $U$, and a row-order array $p$. It targets matrices whose pivots pass the chosen tolerance; it rejects singular matrices and may also reject nonsingular matrices with very small pivots relative to the input scale.
Use zero-based indices throughout. Define $P$ by $P_{i,p_i}=1$, with all other entries zero. Consequently, row $i$ of $PA$ is original row $p_i$ of $A$, and $(Pb)_i=b_{p_i}$. This array is the final row ordering, rather than a log of individual swaps. The code applies it directly without constructing a dense permutation matrix.
Triangular shape makes substitution possible. In $Ly=c$, row $i$ contains only the already-known values $y_0,\ldots,y_{i-1}$ and the new value $y_i$, whose coefficient is $1$. In $Ux=y$, start at the last row and work upward, dividing by each nonzero diagonal entry of $U$.
The LAPACK DGETRF reference documents partial pivoting, unit lower triangular factors, and a production implementation based on matrix blocks. Its displayed convention is $A=P_{\mathrm{LAPACK}}LU$; our convention uses $P=P_{\mathrm{LAPACK}}^{-1}$, so check the direction of a library's permutation before copying its solve formula. LAPACK's pivot indices describe sequential interchanges, unlike the final ordering returned here.
| Term | Exact meaning in this lesson | Worked example or purpose |
|---|---|---|
| Coefficient matrix $A$ | Original $n\times n$ input, preserved by the code | Rows $(0,2,1)$, $(1,1,0)$, $(2,1,1)$ |
| Permutation array $p$ | Row $i$ of $PA$ comes from row $p_i$ of $A$ | Final $p=(2,0,1)$; thus $Pb=(b_2,b_0,b_1)^T$ |
| Unit lower triangular $L$ | $L_{ii}=1$ and $L_{ij}=0$ for $j\gt i$ | Stores the final, correctly permuted elimination multipliers |
| Upper triangular $U$ | $U_{ij}=0$ for $i\gt j$ | Final diagonal is $(2,2,-3/4)$ |
| Pivot | Entry used to eliminate below the active diagonal | At $k=1$, choose $2$ instead of $1/2$ |
| Partial pivoting | Select a largest absolute entry in the active column among rows $k,\ldots,n-1$ | At $k=0$, original row $2$ moves to the top |
| Multiplier $m_{ik}$ | Ratio $U_{ik}/U_{kk}$ before clearing $U_{ik}$ | After the second swap, $m_{21}=(1/2)/2=1/4$ |
| Relative pivot test | Reject if $\lvert U_{rk}\rvert/\alpha\le\tau$, where $\alpha=\max_{i,j}\lvert A_{ij}\rvert$ | Here $\alpha=2$ and $\tau=10^{-12}$ |
| Factor residual | Largest entrywise magnitude of $LU-PA$ | Must compare with permuted $A$, rather than original $A$ |
| Solve residual | $\lVert Ax-b\rVert_\infty$ for the original system | Checks that permutation and substitutions were applied correctly |
How LU Decomposition Works, Step by Step
Start with $U=A$, $L=I$, and $p=(0,1,\ldots,n-1)$. Keep the original scale $\alpha=\max_{i,j}\lvert A_{ij}\rvert$ fixed throughout factorization. Reject $\alpha=0$ immediately. At the beginning of stage $k$, the columns before $k$ already have zeros below their diagonal in $U$.
- Select a pivot. Search rows $k,\ldots,n-1$ for a maximum of $\lvert U_{ik}\rvert$. Keep the first row on a tie, so the implementations agree. If the chosen pivot satisfies $\lvert U_{rk}\rvert/\alpha\le\tau$, stop with an error before dividing.
- Move the equation and its history. If $r\ne k$, swap entire rows $k$ and $r$ of $U$, swap $p_k$ and $p_r$, and swap only entries in columns $0,\ldots,k-1$ between rows $k$ and $r$ of $L$. Those entries contain previously recorded multipliers. Leave the diagonal and not-yet-filled columns of $L$ in place.
- Store each multiplier. For every $i\gt k$, set $L_{ik}=U_{ik}/U_{kk}$. Save this ratio before setting $U_{ik}=0$.
- Update the trailing row. For every $j\gt k$, replace $U_{ij}$ by $U_{ij}-L_{ik}U_{kj}$. The pivot row is unchanged. In this Doolittle-style arrangement, we do not normalize its diagonal to $1$.
- Advance. Repeat for $k+1$. The final stage still checks its pivot even though no rows remain below it. Return the completed factors and permutation.
Once the factors exist, a solve uses
$$ y_i=b_{p_i}-\sum_{j=0}^{i-1}L_{ij}y_j, \qquad x_i=\frac{y_i-\sum_{j=i+1}^{n-1}U_{ij}x_j}{U_{ii}}. $$
Evaluate the first recurrence from top to bottom and the second from bottom to top. Empty sums are zero. A later right-hand side reuses the same $L$, $U$, and $p$; only $y$ and $x$ are recomputed.
flowchart TD
A["Validate A and tolerance; initialize L, U, p"] --> B{"More pivot columns?"}
B -->|Yes| C["Choose largest active column entry"]
C --> D{"Pivot passes relative test?"}
D -->|No| E["Report rejected pivot"]
D -->|Yes| F["Swap U rows, p entries, and earlier L entries"]
F --> G["Store multipliers; update trailing submatrix"]
G --> H["Advance pivot column"]
H --> B
B -->|No| I["Keep factors for reuse"]
I --> J["Permute b; forward solve L; back solve U"]
J --> K{"Another right-hand side?"}
K -->|Yes| J
K -->|No| M["Check original-system residuals"]
Worked Example with a Full Trace
Use
$$ A=\begin{pmatrix}0&2&1\\1&1&0\\2&1&1\end{pmatrix}, \qquad b=\begin{pmatrix}7\\3\\7\end{pmatrix}. $$
Initialization. Set $U=A$, $L=I_3$, $p=(0,1,2)$, and $\alpha=2$. The right-hand side remains untouched during factorization.
Stage $k=0$: first row swap and two eliminations. The candidate magnitudes are $(0,1,2)$, so choose row $2$. Swap rows $0$ and $2$ of $U$, and update $p=(2,1,0)$. There are no earlier columns of $L$ to swap. At this point,
$$ U=\begin{pmatrix}2&1&1\\1&1&0\\0&2&1\end{pmatrix}. $$
The pivot ratio is $2/2=1$, which passes the tolerance. For row $1$, store $m_{10}=1/2$ in $L_{10}$ and subtract half of row $0$: $(1,1,0)-(1/2)(2,1,1)=(0,1/2,-1/2)$. For row $2$, store $m_{20}=0$; its entries stay $(0,2,1)$. The completed first stage is
$$ L=\begin{pmatrix}1&0&0\\1/2&1&0\\0&0&1\end{pmatrix}, \qquad U=\begin{pmatrix}2&1&1\\0&1/2&-1/2\\0&2&1\end{pmatrix}, \qquad p=(2,1,0). $$
Stage $k=1$: move an earlier multiplier. The active column entries are $1/2$ and $2$, so swap rows $1$ and $2$ of $U$. Update $p=(2,0,1)$. Crucially, also swap $L_{10}=1/2$ with $L_{20}=0$; leave both diagonal ones alone. Immediately after this swap,
$$ L=\begin{pmatrix}1&0&0\\0&1&0\\1/2&0&1\end{pmatrix}, \qquad U=\begin{pmatrix}2&1&1\\0&2&1\\0&1/2&-1/2\end{pmatrix}. $$
The pivot ratio is again $1$. Store $m_{21}=(1/2)/2=1/4$. Row $2$ becomes $(0,1/2,-1/2)-(1/4)(0,2,1)=(0,0,-3/4)$.
Stage $k=2$: final pivot check. The remaining pivot is $-3/4$. Its relative magnitude is $(3/4)/2=3/8\gt10^{-12}$, so it passes. No row remains to eliminate. The final factors are
$$ L=\begin{pmatrix}1&0&0\\0&1&0\\1/2&1/4&1\end{pmatrix}, \qquad U=\begin{pmatrix}2&1&1\\0&2&1\\0&0&-3/4\end{pmatrix}. $$
Check the factorization directly:
$$ LU=\begin{pmatrix}2&1&1\\0&2&1\\1&1&0\end{pmatrix} =PA, \qquad P=\begin{pmatrix}0&0&1\\1&0&0\\0&1&0\end{pmatrix}. $$
Forward substitution. Permute the original vector once: $Pb=(7,7,3)^T$. Then $y_0=7$, $y_1=7$, and $y_2=3-(1/2)7-(1/4)7=-9/4$.
Back substitution. Compute $x_2=(-9/4)/(-3/4)=3$, then $x_1=(7-3)/2=2$, and finally $x_0=(7-2-3)/2=1$. Hence $x=(1,2,3)^T$, and direct multiplication gives $Ax=(7,3,7)^T=b$.
Reuse the factors. For $b^{(2)}=(0,1,2)^T$, the permuted vector is $(2,0,1)^T$, the forward result is $y^{(2)}=(2,0,0)^T$, and the back solve gives $x^{(2)}=(1,0,0)^T$. No elimination is repeated. Two row swaps also give $\det(A)=(-1)^2(2)(2)(-3/4)=-3$.
Trace It by Hand
Pause just before stage $k=1$, when $p=(2,1,0)$ and the first column of $L$ is $(1,1/2,0)^T$. Predict which active row becomes the pivot, where the stored $1/2$ must move, and the last entry of $U$ after elimination.
Reveal: row $2$ moves to position $1$ because $2\gt1/2$. The old $L_{10}=1/2$ moves to $L_{20}$, while $L_{10}$ becomes $0$. The new multiplier is $L_{21}=1/4$, and the last diagonal entry becomes $-1/2-(1/4)(1)=-3/4$.
Now reuse those factors for $b^{(3)}=(1,0,1)^T$. Predict the forward vector before back substitution. Check: $Pb^{(3)}=(1,1,0)^T$, $y=(1,1,-3/4)^T$, and $x=(0,0,1)^T$, which is confirmed by the third column of $A$.
Implementation in Python, C++, and Java
Each panel is a complete program using ordinary nested arrays, explicit partial pivoting, and the same sample data. Run Python 3 directly; compile the C++ program as C++14 or newer; save the Java program as Main.java. The factorization copies $A$ and returns separate $L$, $U$, and $p$ arrays. The solve function accepts an unchanged factorization result and preserves its factors and the input vector.
The default tolerance is $\tau=10^{-12}$. The pivot test compares against the original maximum entry magnitude, so uniformly scaling $A$ leaves the test unchanged in exact arithmetic. This simple threshold is a policy for rejecting small pivots, not a condition-number estimate or a general numerical-rank algorithm. Equilibration or a different tolerance may be appropriate when row and column scales differ greatly. The programs reject invalid dimensions, nonfinite data, rejected pivots, and detected nonfinite arithmetic; they assume real values representable as ordinary floating-point numbers.
The factor error is $\max_{i,j}\lvert(LU)_{ij}-A_{p_i,j}\rvert$. The solve error is the maximum absolute component of $Ax-b$, computed against the original input. These are absolute residuals. The sample uses an absolute pass threshold of $10^{-10}$ because its data have modest scale; the threshold is not a universal accuracy criterion. Printing converts signed zero to ordinary zero for consistent output without changing computations.
All three programs print the same nine lines:
permutation = [2, 0, 1]L = [[1.000000, 0.000000, 0.000000], [0.000000, 1.000000, 0.000000], [0.500000, 0.250000, 1.000000]]U = [[2.000000, 1.000000, 1.000000], [0.000000, 2.000000, 1.000000], [0.000000, 0.000000, -0.750000]]x = [1.000000, 2.000000, 3.000000]reused x = [1.000000, 0.000000, 0.000000]factor residual = 0.000e+00solve residual = 0.000e+00reuse residual = 0.000e+00check = PASS
Complexity, Correctness, and Invariants
For a successful dense $n\times n$ input, these implementations take $\Theta(n^3)$ factorization time in the best, average, and worst cases. They execute the trailing updates even when a multiplier is zero, so an identity matrix still follows the same cubic loop structure. The leading arithmetic count is about $2n^3/3$ floating-point operations, counting a multiplication and subtraction separately. Pivot searches and accumulated row/prefix swaps contribute $O(n^2)$ work.
The returned factors and working copy require $\Theta(n^2)$ space in total, with $O(n)$ permutation storage. Each solve takes $\Theta(n^2)$ time and $\Theta(n)$ additional space. Factor once and solve $q$ right-hand sides for total time $\Theta(n^3+qn^2)$; solving them sequentially needs only one solve workspace at a time. Refactoring for every right-hand side instead costs $\Theta(qn^3)$. An early pivot rejection can stop sooner, although input validation and copying already take quadratic work on valid square inputs.
| Method | Preparation time | Each later right-hand side | Storage | Key assumption or distinction |
|---|---|---|---|---|
| Dense partial-pivoted LU here | $\Theta(n^3)$ | $\Theta(n^2)$ | $\Theta(n^2)$ factors; $\Theta(n)$ solve workspace | Square matrix with accepted pivots; reusable $PA=LU$ |
| Gaussian elimination repeated from scratch | $\Theta(n^3)$ for each solve | $\Theta(n^3)$ if earlier factors were discarded | $\Theta(n^2)$ work matrix | Elimination becomes reusable LU when multipliers and swaps are retained |
| Gauss-Jordan on each new augmented system | $\Theta(n^3)$ for each solve | $\Theta(n^3)$ in this repeated approach | $\Theta(n^2)$ | Eliminates above and below pivots; useful for RREF |
| Dense Cholesky | $\Theta(n^3)$, about $n^3/3$ operations | $\Theta(n^2)$ | $\Theta(n^2)$ | Symmetric positive definite input; factor $A=CC^T$ |
| Dense Householder QR | $\Theta(n^3)$ for square input | $\Theta(n^2)$ with stored transformations | $\Theta(n^2)$ | Also handles overdetermined full-column-rank least squares |
For correctness, reason first in exact arithmetic with nonzero accepted pivots. At each stage, keep the evolving identity $PA=LU$, with identity entries in the unfilled part of $L$ and the current partially reduced matrix in $U$. Initially $P=I$, $L=I$, and $U=A$, so the identity holds.
A swap matrix $S$ exchanging active rows $k$ and $r$ changes $P$ to $SP$ and $U$ to $SU$. Swapping the earlier entries of those rows in $L$ is equivalent, in its current partially filled shape, to replacing $L$ by $SLS$. Therefore $(SLS)(SU)=SLU=SPA$. This is why the stored multipliers must move and why swapping whole rows of $L$ would be wrong.
Next, subtracting $m_{ik}$ times pivot row $k$ from row $i$ of $U$ is multiplication by an elementary elimination matrix $E$. Its inverse adds that multiple back. Recording $m_{ik}$ in the new column of $L$ effects $L\leftarrow LE^{-1}$ while $U\leftarrow EU$, preserving the product. Already completed columns of $U$ remain zero below their diagonal because both active rows have zeros there. At termination, $U$ is upper triangular and $L$ is unit lower triangular.
Finally, $Ly=Pb$ and $Ux=y$ imply $LUx=Pb$, hence $PAx=Pb$, and invertibility of $P$ gives $Ax=b$. Nonzero pivots make the triangular solution unique. Floating-point arithmetic yields approximate identities instead of exact ones; partial pivoting limits multipliers to magnitude at most $1$ but does not remove all possible element growth or sensitivity to ill conditioning.
Maintain $PA=LU$ while successive columns of $U$ acquire zeros below their pivots. Every active-row swap must also move the earlier multiplier entries in $L$; every elimination stores the multiplier that reverses that operation. The final triangular solves then recover a solution of the original system.
Variants, Pitfalls, and Optimizations
Permutation direction is part of the interface. With $PA=LU$, solve using $Pb$, meaning b[p[i]]. For the final ordering $(2,0,1)$, this gives $(b_2,b_0,b_1)$; applying the inverse order would generally give a different vector. A library's sequential pivot log must be applied as swaps, not treated as this final indexing array.
A later pivot must move earlier multipliers. In the worked example, omitting the prefix swap leaves the wrong matrix
$$ L_{\mathrm{bad}}=\begin{pmatrix}1&0&0\\1/2&1&0\\0&1/4&1\end{pmatrix}. $$
Using it with the otherwise correct final $U$ and $p$ gives a factor residual of $1$. For the original $b$, forward substitution gives $y_{\mathrm{bad}}=(7,7/2,17/8)^T$, and the resulting solution is $x_{\mathrm{bad}}=(10/3,19/6,-17/6)^T$. Its original-system residual is $\lVert Ax_{\mathrm{bad}}-b\rVert_\infty=7/2$. A triangular-looking result alone cannot establish correctness.
Choice of factorization and storage. Doolittle fixes the diagonal of $L$ to ones; Crout instead fixes the diagonal of $U$. Packed LU stores strict-lower multipliers and upper-triangular entries in a single matrix, with the unit diagonal of $L$ implicit. In packed storage a whole row swap moves both types of stored data, while our separate arrays require a whole-row swap in $U$ and a prefix swap in $L$.
Dense, sparse, and structured cases. Blocking groups trailing updates into matrix products for better cache use and optimized BLAS execution. Sparse LU adds ordering and symbolic analysis to control fill-in, since zeros can become nonzeros. Cholesky is attractive for symmetric positive definite systems; QR or SVD better fits many least-squares and rank-sensitive tasks. Unpivoted LU needs additional guarantees; a zero leading entry does not by itself make a matrix singular.
Numerical checks and reuse. A small pivot under this threshold may reflect poor scaling rather than exact singularity: $\operatorname{diag}(1,10^{-14})$ is nonsingular but rejected with $\tau=10^{-12}$. Conversely, passing the test does not certify good conditioning. A useful scale-aware solve diagnostic is
$$ \eta=\frac{\lVert Ax-b\rVert_\infty} {\lVert A\rVert_\infty\lVert x\rVert_\infty+\lVert b\rVert_\infty}, \qquad \lVert A\rVert_\infty=\max_i\sum_j\lvert A_{ij}\rvert. $$
If the denominator is zero, treat an exactly zero residual separately. A small backward residual can coexist with a large solution error for an ill-conditioned matrix. Iterative refinement reuses the factors to solve a correction equation from a newly evaluated residual. Reuse applies while $A$ stays fixed; changing its entries generally requires new factors. To solve $Ax=b$, apply the factors directly instead of first forming an explicit inverse.
Keep the permutation convention explicit, move only the completed prefix of each swapped row in $L$, and check the last pivot before back substitution. Preserve original inputs for residual checks, and interpret a tolerance rejection as a numerical decision rather than an exact rank classification.
Practice Problems and Real-World Use
Start with hand traces where every multiplier is a simple fraction. Then test a factorization independently from its solve routine: verify triangular shape, the permutation, $LU\approx PA$, and $Ax\approx b$ for known solutions. Include a negative one-by-one matrix, a swap at a later pivot, a nonsingular matrix rejected at the default tolerance, and a singular matrix whose final pivot is zero.
For a reusable solver, choose several known vectors $x^{(t)}$, compute $b^{(t)}=Ax^{(t)}$, factor $A$ once, and recover every vector from the same factors. Check residuals with an appropriate scale normalization as the matrix size and magnitude change. This separates factorization bugs from permutation or substitution bugs.
| Exercise or system | Concrete task and expected checkpoint | Idea tested | Difficulty |
|---|---|---|---|
| Two-by-two first pivot | Factor rows $(0,2)$ and $(1,3)$; solve $b=(4,7)^T$ to obtain $x=(1,2)^T$ | Row swap before division; correct $Pb$ | Beginner |
| Later pivot regression | Reproduce this lesson's $3\times3$ factors and $p=(2,0,1)$ | Move the earlier $1/2$ multiplier in $L$ | Intermediate |
| Reusable solver | Use $b^{(3)}=(1,0,1)^T$ with existing factors; recover $(0,0,1)^T$ | Two substitutions without refactorization | Intermediate |
| Determinant calculator | Count swaps and compute $(-1)^s\prod_i U_{ii}$; obtain $-3$ here | Permutation parity and unit diagonal of $L$ | Intermediate |
| Singular versus small pivot | Compare rows $(1,2),(2,4)$ with $\operatorname{diag}(1,10^{-14})$ | Exact dependence versus tolerance-based rejection | Intermediate |
| Fixed linear circuit model | Solve several source vectors while the coefficient matrix stays fixed | Amortize one factorization across many excitations | Applied |
| Sparse discretized system | Compare orderings and count fill-in during elimination | Sparse structure changes memory and operation costs | Advanced |
| Iterative refinement | Compute $r=b-Ax$, solve $A\delta=r$ with the factors, update $x+\delta$ | Residual-based correction and conditioning limits | Advanced |