LU Decomposition

Starting from middle-school bike problems and building up to Gaussian elimination, we crack open LU decomposition — because computing A⁻¹ is just painful.

So while we were poking at the Black-Scholes PDE numerically, this kind of matrix operation kept showing up:

$$Ax = b \quad (A: \text{matrix}, \; x \;\&\; b: \text{vectors})$$

What we want is $x$, so all we had to do was find the inverse of $A$. But finding that inverse is hard, and coding it up is even harder. So, to make life… a little easier, we learn LU decomposition — or so they say~

To understand LU decomposition better, I’m gonna start from Gaussian elimination.

Gaussian elimination, middle-school style

Let me explain the idea at a middle-school level. You’ve all solved a problem like this in elementary school:

There are 2-wheeled bikes and 4-wheeled bikes, 100 of them total. But somebody ducked down — whoosh!!! — and for some reason counted only the wheels. It’s already weird that they got an accurate wheel count, but apparently the wheels added up to 354.

How many 2-wheeled bikes? How many 4-wheeled bikes?

(In the time it took to count the wheels, you could’ve just counted the bikes lol)

Anyway. When we were kids, we’d start by guessing 50/50, then think about the wheel count — “should there be fewer 4-wheelers? fewer 2-wheelers?” — and inch our way to the answer.

But Korea’s dreaded 8th graders fear nothing. They just slam equations down. The 8th grader slams them down like this:

The bike equations: x + y = 100 and 2x + 4y = 354

Set up the equations. Then — call it elimination, call it substitution, whatever — they double the top equation and line things up like this:

2x + 2y = 200 and 2x + 4y = 354

And then write this:

−2x − 2y = −200 and 2x + 4y = 354

And then they just barrel through to the next step like an automated machine:

2y = 154, so y = 77: 77 four-wheelers and 100 − 77 = 23 two-wheelers

Now this 8th grader grows up. Goes to college. Takes linear algebra (the required class in basically every STEM major). And now they’d solve it like this. (Of course nobody actually solves it this way lol)

The same system again: x + y = 100 and 2x + 4y = 354

Rewritten as:

The same system as a matrix equation: (1 1; 2 4)(x, y) = (100, 354)

Multiply the top row by 2:

Top row times 2: (2 2; 2 4)(x, y) = (200, 354)

Multiply the top row by $(-1)$:

Top row times −1: (−2 −2; 2 4)(x, y) = (−200, 354)

Replace row 2 with (row 2 + the top row):

↓

Row 2 plus row 1: (−2 −2; 0 2)(x, y) = (−200, 154)

And replace the top row with (top row + row 2):

↓

Row 1 plus row 2: (−2 0; 0 2)(x, y) = (−46, 154)

Multiply both rows by $1/2$ (and for row 1, also multiply by $-1$):

(1 0; 0 1)(x, y) = (23, 77)

$$\therefore x=23,\; y=77$$

──── tangent warning ────

If you’re seeing this for the first time, you might be staring at the red arrows above going “…wait, why is that allowed?”

Because I didn’t get it the first time either, heh. The reason it’s allowed comes down to properties of equality: if you have an equality $A = B$, you can add or subtract the same thing $C$ on both sides.

So in this equation

−2x − 2y = −200 and 2x + 4y = 354

think of it like this:

Note: treat −2x − 2y as one lump, a number equal to −200

Now let’s clearly add the same number to both sides of the bottom equality. We’ll add $-200$ to both the left side and the right side:

Adding −200 to both sides of the second equation

But that $-200$ on the left of the bottom equation? Let’s rewrite it as a different number in disguise:

The −200 on the left written as the lump −2x − 2y

−2x − 2y = −200 and 0x + 2y = 154

That’s how you can explain the red arrows: adding or subtracting $k$ times one row from another is allowed. lol

──── tangent over ────

The compressed version of why all this is allowed: because it’s an equality! That’s it. But OK, let me say it the fancier way.

The coefficient matrix (1 1; 2 4)

This matrix finally turned into the form

The identity matrix (1 0; 0 1)

But — what if (just let this wash over you) it had been

A messy 5 × 10 matrix of numbers

this matrix??? Through the same kind of “elimination”, it would’ve had to become:

The same matrix after elimination: leading 1s, zero rows at the bottom

Let me describe what the “form we’re trying to reach” looks like:

Leading 1s circled: a row can start with 0s but its first nonzero entry is 1, zero rows at the bottom, zeros below each leading 1

And of course this shape, just like above, is reached using only the properties of equality. There’s more detail in Systems of Linear Equations and Gaussian Elimination (Linear Algebra I Studied #9).

A matrix with those features is called a row echelon form (when every leading 1 also has only zeros above it, like the one above, it’s the reduced row echelon form, RREF). And to turn a matrix into that form using the properties of equality, you get exactly three moves:

  1. Multiply a row by a (nonzero) constant.
  2. Swap two rows (or swap them after scaling).
  3. Add a constant multiple of one row to another row.

That’s it. Those are the only moves. (These three are called the transformation rules.) Yep yep yep — using only these to bring a matrix into row echelon form is what we call Gaussian elimination.

Don’t lose the thread, by the way: we started from Gaussian elimination in order to learn LU decomposition.

EROMs — the matrices that do the elimination for you

Whoa!!! But!!! Here’s the thing about those transformation rules.

Swapping two rows:

Swapping the two rows of an augmented matrix

Doesn’t matter, right?? Even doing it like this??? But you can also say that this swap means multiplying by the matrix $\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}$~~~:

The swap as multiplication by (0 1; 1 0)

Picking one row of a matrix and multiplying it by a constant:

Scaling the first row by r

Doing that doesn’t break the system of equations, right??? But that scaling is also the same as multiplying by the matrix $\begin{pmatrix} r & 0 \\ 0 & 1 \end{pmatrix}$~~~~:

The scaling as multiplication by (r 0; 0 1)

Finally, adding a constant multiple of one row to another row:

Adding r times row 1 to row 2

Doing this doesn’t break the system either, right?!? But the move is also the same as multiplying by $\begin{pmatrix} 1 & 0 \\ r & 1 \end{pmatrix}$~~~:

The row addition as multiplication by (1 0; r 1)

These transformation rules — even for a big~~~ matrix — are really “pick two rows out of many and do that thing.” That’s why I drew them above as $2 \times n$ matrices with two rows. The point I’m making here is not “eyeball it and figure it out.” The point is:

The act of left-multiplying by a certain matrix is itself the act of applying a transformation rule.

That’s what I want to land.

cf. You might be thinking “OK but does this work for big matrices?” To help, here’s a 4×4 example. To swap row 2 and row 3 in a 4×4 matrix:

4 × 4 matrix that swaps rows 2 and 3

— same as multiplying by this matrix. To multiply row 3 by $r$:

4 × 4 matrix that multiplies row 3 by r

— same as multiplying by this matrix. To add $r \times$ row 1 to row 4:

4 × 4 matrix that adds r times row 1 to row 4

— same as multiplying by this matrix. Keep that in mind for matrices with more than 2 rows, too.

In short, a special matrix that carries out an elementary row operation like this is called an Elementary Row Operation Matrix (EROM), R-hat.

From row echelon form to upper-triangular to LU

Ahhhhh, so. If you keeeeeeep multiplying EROMs onto some wild messy not-pretty-at-all matrix (= keep applying transformation rules), it turns into a pretty row echelon form.

But!!! What if that row echelon form is a square $n \times n$ matrix???

A square echelon form: zeros below the diagonal, anything above it — an upper triangular matrix

Then can’t we call it an upper triangular matrix?!?!

In an equation made of matrices, Ax = b, say matrix $A$ reaches row echelon form after $k$ transformation rules. That is — say it becomes upper triangular after $k$ rules. So: R¹ ⋯ Rᵏ — multiplying these EROMs a finite number of times,

(R¹ ⋯ Rᵏ)Ax = (R¹ ⋯ Rᵏ)b

— like this — turns $A$ (if it’s an $n \times n$ square matrix) into an upper triangular matrix. We just confirmed that.

So let’s take this guy, (R¹ ⋯ Rᵏ)A, as one big lump and call it U. Then we’ve got Ux = (R¹ ⋯ Rᵏ)b.

Now — each of those individual $R$’s is, no matter what, an invertible matrix.

(Why each one is unconditionally invertible — explaining that would take forever T_T. I’ll leave that to linear algebra… probably you’d want to start from my “Linear Algebra I Studied #8” onward. But to follow #8 you’d want the stuff before #8 too, which is why I’m hesitating to drop a link… so I won’t… heh heh.)

Anyway, so:

Ux = (R¹ ⋯ Rᵏ)b, so (R¹ ⋯ Rᵏ)⁻¹Ux = b

what this means is — this is possible.

For LU, stop after forward elimination, once the matrix is upper triangular. Do not keep eliminating above the pivots to reach RREF. For now, assume the pivots are nonzero and no row swaps are needed.

Woohoo…… OK now — since (R¹ ⋯ Rᵏ)⁻¹ becomes a lower triangular matrix, LUx = b is the conclusion, so it looks like all I have left to explain is why (R¹ ⋯ Rᵏ)⁻¹ again becomes a lower triangular matrix.

First — the row-scaling and below-pivot row-addition EROMs we use for forward elimination are lower triangular. → Scroll back up and check!! heh heh. General row operations need not have this shape.

Then two facts:

  1. The inverse of a row swap is that same row swap.

    P → P⁻¹ = P : permutation

  2. The inverse of a nonsingular lower triangular matrix is also lower triangular.

OK so here’s how it really has to be phrased. In Ax = b, the operations R¹ ⋯ Rᵏ can include row swaps. Keeping track of those swaps gives $PA=LU$, so we solve $LUx=Pb$. We cannot simply move all swaps to the front without adjusting the other factors.

(PA)x = Pb, written A′x = b′

Ah but — if we rename the suitably permuted system Ax = b, then this renamed $A$ has an $LU$ decomposition when its elimination pivots are nonzero! The original matrix need not have an LU decomposition without row swaps.

(R¹ ⋯ Rᵏ)Ax = (R¹ ⋯ Rᵏ)b, Ux = (R¹ ⋯ Rᵏ)b, (R¹ ⋯ Rᵏ)⁻¹Ux = b, LUx = b

Yayyyy~~~ $A$ → decomposable into $LU$!!!!

Why we wanted this in the first place

Quick reminder of why we’re learning LU decomposition in the first place. In this matrix equation, Ax = b, using the inverse, x = A⁻¹b — this is hard to code.

So instead, in Ax = b our goal is just to find vector $x$ anyway, so if we change that equation into the form LUx = b, we can find $x$ without ever coding an inverse matrix.

Let Ux = z

L(Ux) = b becomes Lz = b: find z first because L is lower triangular, then x from Ux = z because U is upper triangular

Now the actually hardcore part

OK NOW it gets really hellish lol

What I mean is: given a matrix $A$, we need to write code that computes the elements of $L$ and $U$ in the LU decomposition. First —

A = (a11 … a33), a 3 × 3 matrix

we saw this matrix being LU-decomposed like this:

= (l11 0 0; l21 l22 0; l31 l32 l33)(1 u12 u13; 0 1 u23; 0 0 1)

(Because if you do Gaussian elimination, the leading 1s land like that in the $U$ matrix?? That is — since I’m assuming the diagonal of $U$ is 1, it’d be right to call this the Crout method.)

Let’s look at how to code $L$.

A = LU for a 3 × 3 matrix, multiplied out

What this is saying right now is

LU multiplied out, entry by entry, equals A

— so we can express $l_{ij}$ in terms of $u_{ij}$ and $a_{ij}$!!!

I mean, if you brute-force Gaussian elimination on $Ax = b$, it becomes $Ux = (RR \dots R)b$, and at that point we already know both $u$ and $a$, right? So we don’t know exactly what $(RRR \dots R)$ looks like, and in these trying times finding its inverse, the lower triangular matrix, is a pain — what this means is, we can just slam $L$ in using only $u_{ij}$ and $a_{ij}$.

Ah but, that would mean we’d have to do Gaussian elimination on $A$ by hand first and then code, which is also kinda not right.

So from here on, I’m gonna show you what I’ll call the swoosh-swoosh-swoosh calculation method. Instead of finding everything in one big BANG!!!~~~ — one swoosh from $L$, one swoosh from $U$, then one more swoosh from $L$, one more swoosh from $U$ — if we go sequentially like that, we can take matrix $A$ and rrrrrrrrrroll right through it, defining every element of $L$ and $U$.

Might be hard to grok at first, but look at it carefully once and it’ll click lol. OK let’s go. I’ll keep it easy with a 3×3 like above, then generalize to $n \times n$.

LU multiplied out, entry by entry, equals A

Looking at this equation first,

Column 1 of L: l11 = a11, l21 = a21, l31 = a31

— these relations fall right out. Putting them all together, let’s call it l_i1 = a_i1.

So now what’s left is the $(2,2)$, $(3,2)$, $(3,3)$ entries.

Columns 2 and 3 of L: l22 = a22 − l21 u12, l32 = a32 − l31 u12, l33 = a33 − (l31 u13 + l32 u23), so l_ij = a_ij − Σ l_ik u_kj

I organized things for $L$; let’s organize $U$ too.

LU multiplied out again

Looking at the figure again:

Row 1 of U: u12 = a12/l11, u13 = a13/l11

Row 2 of U: u23 = (a23 − l21 u13)/l22, and in general u_ij = (a_ij − Σ l_ik u_kj)/l_ii

As you can see, $l$ and $u$ are tangled into each other. So when coding, “BANG! define all of $L$, then BANG!!! define all of $U$” just won’t cut it for solving $Ax = b$ with LU decomposition.

So what does it mean to use the swoosh-swoosh-swoosh sequential method? We already know we can rewrite it as $LUx = b$. And while we don’t know $L$ and $U$ exactly yet,

The two formulas: l_ij = a_ij − Σ l_ik u_kj and u_ij = (a_ij − Σ l_ik u_kj)/l_ii

— this much, we do know.

So what’s the answer;;;;

L and U as empty grids

When you fill in these blanks — don’t fill in all of $L$~~~ and then all of $U$ — don’t do that.

The fill-in order: down column 1 of L and along row 1 of U, then column 2 and row 2, and so on

One from $L$, one from $U$. One from $L$, one from $U$.

Find them in that direction, and by rrrrrrrrrolling through it~~~~~ that way, you fill in all of $L$ and $U$.

The two formulas again

As this equation shows:

  • Column 1 of $L$ can be found from $A$ alone.
  • Row 1 of $U$ can be found entirely from the $(1,1)$ element of $L$ and $A$.
  • Column 2 of $L$ needs row 1 of $U$. But row 1 of $U$ was already conquered, so we can use it.
  • Look at row 2 of $U$ too — the same way, the materials for it were prepped just before. heh heh.

Kyahaha! Aha-ha-ha.

So now it makes sense that you can code your way from $Ax = b$ all the way to $LUx = b$. Alright — all that’s left is heading toward the solution, vector $x$. Let’s cover just that and then go straight to how the real coding goes! heh heh.

Forward sub, then back sub

I mentioned this briefly above.

──── copy-paste warning ────

Quick reminder of why we’re learning LU decomposition in the first place. In this matrix equation, Ax = b, using the inverse, x = A⁻¹b — this is hard to code.

So instead, in Ax = b our goal is to find vector $x$ anyway, so if we change that equation into the form $LUx = b$, Ax = b we can find $x$ without ever coding an inverse matrix.

Let Ux = z

L(Ux) = b becomes Lz = b: find z first, then x

──── copy-paste over ────

Like I said, I mentioned it briefly before. Now that we’ve finished coding $LU$, let’s use the logic above and head toward the solution, vector $x$.

So first let’s look at it as $Ux = z$!!! That makes it $Lz = b$, right?!?!

Lz = b written out with L lower triangular

We can see it in this form, and since we now know both $L$ and $U$,

First row: l11 z1 = b1, so z1 = b1/l11

Looking at row 2:

Second row: l21 z1 + l22 z2 = b2, so z2 = (b2 − l21 z1)/l22

Oh nice — for row 2, since we could find $z_1$ just before, we can find $z_2$ too~~~ I’ll do just row 3 and then generalize.

Third row: z3 = (b3 − (l31 z1 + l32 z2))/l33

Now generalizing to $z_i$,

In general: z_i = (b_i − Σ l_ik z_k)/l_ii

we can generalize it like this!!! From $z_1$ all the way to $z_n$, sequentially, all found!!!~~~

And now let’s head to $Ux = z$.

Ux = z written out with U upper triangular

We can see it looks like this!!! This time, unlike before, it’s an upper triangular matrix — that is, the bottom row has the most zero terms!!! So we’ll go the way of finding things from the bottom row up, one at a time. (We currently have all of $z_1$ through $z_n$.)

Last row: u_nn x_n = z_n, so x_n = z_n/u_nn

Ooh, $x_n$ we can pin down!!! Let’s try the row above lol:

Row n−1: x_(n−1) = (z_(n−1) − u_(n−1,n) x_n)/u_(n−1,n−1)

Ahh~~ since we have $x_n$, this is a form where we can find $x_{n-1}$ too!!!

But right now I can’t say it in concrete numbers — I have to keep saying $n$-this and $n$-that, which is a real headache. And generalizing from right here is a bit much, so let me do just one more, OK lol hoo hoo hoo (T_T). Next up is the row above that, row $n-2$:

Row n−2: x_(n−2) = (z_(n−2) − (u_(n−2,n−1) x_(n−1) + u_(n−2,n) x_n))/u_(n−2,n−2)

This is so painfully annoying lol hoo hoo… now quick, let’s generalize. Generalize!

In general: x_i = (z_i − Σ u_ik x_k)/u_ii, k from i+1 to n

Whoa — the principle is finally done???

The actual code

So now… it’s time to code. But honestly… could I ever have come up with this code myself….? (T_T) I think this is gonna be more of an “understanding the code you’re given” kind of session!!!

This is a function procedure: toss in $A$ and $b$ from $Ax = b$, and out pops here’s your solution $x$~~!!! Here it is!

This teaching routine does not do pivoting: it needs nonzero, reasonably sized pivots. A nonsingular matrix can still fail here if a pivot is zero; use pivoted LU for general problems. The VBA version also assumes arrays start at index 0.

Function Solve_SE(matA() As Double, matb() As Double) As Double()

Dim rowA As Long, rowB As Long, colA As Long, n As Long, _
    j As Long, i As Long, k As Long, No As Long
Dim L() As Double, U() As Double, Lsum As Double, Usum As Double, _
    modB() As Double, sumofB As Double, x() As Double, sum As Double, matx As Double

rowA = UBound(matA(), 1)
colA = UBound(matA(), 2)
rowB = UBound(matb(), 1)

If (rowA <> colA) Or (rowB <> colA) Then Exit Function

ReDim L(rowA, colA) As Double
ReDim U(rowA, colA) As Double

For j = 0 To UBound(L, 1)
    For i = 0 To UBound(L, 1)
        If j = 0 Then
           L(i, j) = matA(i, j)
           U(j, i) = matA(j, i) / L(0, 0)

        Else
           If i < j Then
              L(i, j) = 0
           Else
              Lsum = 0
              For k = 0 To j - 1
                  Lsum = Lsum + L(i, k) * U(k, j)
              Next
              L(i, j) = matA(i, j) - Lsum
           End If

          If i < j Then
             U(j, i) = 0
          ElseIf i = j Then
             U(j, i) = 1
          Else
             Usum = 0

             For k = 0 To i - 1
                 Usum = Usum + U(k, i) * L(j, k)
             Next
             U(j, i) = (matA(j, i) - Usum) / L(j, j)
          End If
       End If
    Next
Next

ReDim modB(UBound(matb)) As Double
modB(0) = matb(0) / L(0, 0)

For i = 1 To UBound(modB)
    sumofB = 0
    For k = 0 To i - 1
        sumofB = sumofB + L(i, k) * modB(k)
    Next
    modB(i) = (matb(i) - sumofB) / L(i, i)
Next

ReDim x(UBound(modB)) As Double

For No = UBound(x) To 0 Step -1
    sum = 0
    For i = 0 To UBound(x)
        sum = sum + U(No, i) * x(i)
    Next
    x(No) = modB(No) - sum
Next

Solve_SE = x()

End Function

There’s no way to fit this in a single screenshot (T_T), and for it to look like code it has to be a screenshot, so here it is torn into pieces:

Solve_SE in the VBA editor, first half

Solve_SE in the VBA editor, second half

Honestly, I think studying code is something you do on your own, heave-ho, heave-ho — but please just take this as a reference for how I studied this code… T_T

My handwritten notes on the first half of Solve_SE

Handwritten notes: the rest of the U loop

Handwritten notes: the order in which the double loop fills L and U

Handwritten notes: the Σ trick in the L loop

Continuing with the code:

Handwritten notes on the forward and back substitution

It works in Python too, with the same result as in VBA.

Solve_SE in the VBA editor

Solve_SE in the VBA editor, substitution part

Test Sub for the 3 × 3 system, printing 1.00, 1.00, 1.00 in the Immediate window

Solve_SE in Python (PyCharm), first part

Solve_SE in Python, the rest and the test, printing 1, 1, 1

import numpy as np

def Solve_SE(matA, matb):
    rowA = len(matA); colA = len(matA.T);     rowB = len(matb)
    if (rowA != colA) | (rowB != colA):
        raise NotImplementedError
    else:
        pass
    L = np.zeros((rowA, colA))
    U = np.zeros((rowA, colA))
    for j in range(0, len(L.T)):
        for i in range(0, len(L)):
            if j==0:
                L[i, j] = matA[i, j]
                U[j, i] = matA[j, i] / L[0, 0]
            else:
                if i < j:
                    L[i, j] = 0
                else:
                    Lsum = 0
                    for k in range(0, j):
                        Lsum = Lsum + L[i, k]*U[k, j]
                    L[i, j] = matA[i, j] - Lsum
                if i < j:
                    U[j, i] = 0
                elif i == j:
                    U[j, i] = 1
                else:
                    Usum = 0
                    for k in range(0, i):
                        Usum = Usum + L[j, k]*U[k, i]
                    U[j, i] = (matA[j, i] - Usum) /L[j, j]
    modB = np.zeros((len(matb), 1))
    modB[0] = matb[0] / L[0, 0]
    for i in range(0, len(modB)):
        sumofB = 0
        for k in range(0, i):
            sumofB = sumofB + L[i, k]*modB[k]
        modB[i] = (matb[i] - sumofB) / L[i, i]
    x = np.zeros((len(modB), 1))
    for no in range(len(x)-1, 0-1, -1):
        sum=0
        for i in range(0, len(x)):
            sum = sum + U[no, i] * x[i]
        x[no] = modB[no] - sum
    return x

matA=np.zeros((3, 3))
matb=np.zeros((3, 1))
matA[0, 0]=4; matA[0, 1]=-1; matA[0, 2]=0
matA[1, 0]=-1; matA[1, 1]=4; matA[1, 2]=-1
matA[2, 0]=0; matA[2, 1]=-1; matA[2, 2]=4
matb[0]=3
matb[1]=2
matb[2]=3
matx = Solve_SE(matA, matb)
print(matx)

Heads-up. The figures here are the original notes, slips and all. A few things to watch for:

  • In the handwritten notes on the VBA code, three index slips: the L loop adds L(i,k) * U(k,j) (not L(i,j) * U(k,j)), and its branch runs for i >= j, the diagonal included; the U loop adds L(j,k) * U(k,i) and divides by L(j,j) (not L(j,i)); and the back substitution adds U(No,i) * x(i). The typed VBA in the post is right.
  • In the Python screenshot, the U loop should add L[j, k]*U[k, i], not L[k, i]*U[j, k]. The printed [1, 1, 1] comes out right only because the test matrix is tridiagonal; the code block in the post has the corrected line.
  • In the repeated copy-paste block, the system we change to is LUx = b, not Ax = b.
  • For a general permutation matrix, P inverse is P transpose. A single row swap is its own inverse, as used here.

Comments

Discussion happens via GitHub Discussions. You'll need a GitHub account to comment.