Skip to content

Tridiagonal and banded matrices

A matrix whose non-zeros lie on a few central diagonals is stored diagonal by diagonal: 3n − 2 cells for a tridiagonal one, and a system with it is solved in about 8n steps.

Helps to know firstDiagonal matrices

Helps to know first
This topic

First, the picture

A 5 × 5 matrix whose numbers sit on three central diagonals. Watch each diagonal move out into a strip of its own.

Storing three diagonals

A 5 × 5 matrix kept as three strips

static double lo[N - 1], di[N], up[N - 1]; double get(int i, int j){    if (i == j)        return di[i];    if (i == j + 1)        return lo[j];    if (j == i + 1)        return up[i];    return 0.0;}int main(void){    for (int i = 0; i < N; i++)        di[i] = 2;    for (int i = 0; i < N - 1; i++) {        lo[i] = -1;        up[i] = -1;    }    for (int i = 0; i < N; i++) {        for (int j = 0; j < N; j++)            printf("%2g ", get(i, j));        printf("\n");    }    printf("stored %d of %d cells\n", 3 * N - 2, N * N);    const double b[N] = { 0, 0, 0, 0, 6 };    double x[N];    solve(b, x);    printf("x =");    for (int i = 0; i < N; i++)        printf(" %.6g", x[i]);    printf("\n");    return 0;}

A 5 × 5 matrix whose non-zeros sit on three diagonals. First the main diagonal: di[5].

cells stored
13 of 25

Output

0.00 / 9.85 s
Describe this picture

The picture is a 5 × 5 grid with the row and column numbers beside it, and under it three strips: lo of 4 boxes for the diagonal below the main one, di of 5 boxes for the main diagonal, and up of 4 boxes for the diagonal above. The 13 grid cells on the three diagonals are drawn solid; the other 12 are faint and dashed and show 0, because no memory holds them. Each row of the grid has a faint tint, carried down to its boxes in the strips. Under the grid is a line of text naming the cell the program is at. Under the picture are the lines of tridiag.c the clip runs, with the line about to run marked, one sentence on the current step, a readout “cells stored” reading “13 of 25”, and the output. The clip plays once, about 10 seconds. The strips start at 0, since the arrays are statics. First the main diagonal: 2 flies from each of the 5 diagonal cells into di, top to bottom. Then the two side diagonals, a pair at a time: −1 flies from (1, 0) into lo[0] and from (0, 1) into up[0], and so on down to (4, 3) and (3, 4). The program prints the matrix through get, rows 2 −1 0 0 0 to 0 0 0 −1 2, in one beat, and the clip rests on stored 13 of 25 cells. Watch only: there is no control. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as tridiagonal.step.

Three strips

A tridiagonal matrix is zero except on its main diagonal and the two diagonals beside it: A(i, j) = 0 whenever |i − j| > 1. The main diagonal has n entries and each side diagonal has n − 1, so the matrix holds at most

n + 2(n − 1) = 3n − 2

non-zero entries. For n = 5 that is 13 of 25. The program keeps each diagonal in an array of its own:

static double lo[N - 1], di[N], up[N - 1];

di[i] is A(i, i), lo[j] is A(j + 1, j), just below the diagonal, and up[i] is A(i, i + 1), just above it. The program fills di with 2 and lo and up with −1.

The code and its cost

Reading an entry

get asks which diagonal, if any, holds (i, j):

double get(int i, int j)
{
    if (i == j)
        return di[i];
    if (i == j + 1)
        return lo[j];
    if (j == i + 1)
        return up[i];
    return 0.0;
}

get(3, 2) is lo[2] = −1; get(0, 3) is on none of the three diagonals, so it is 0 with no memory read. The program prints all 25 entries through get:

 2 -1  0  0  0
-1  2 -1  0  0
 0 -1  2 -1  0
 0  0 -1  2 -1
 0  0  0 -1  2
stored 13 of 25 cells

Bands

A banded matrix has its non-zeros within w diagonals of the main one: A(i, j) = 0 whenever |i − j| > w. The number w is the half-width; a tridiagonal matrix has w = 1, a pentadiagonal one w = 2. The band holds 2w + 1 diagonals: the main one with n entries, and two of each length n − 1, n − 2, …, n − w. That makes

n + 2((n − 1) + (n − 2) + … + (n − w)) = (2w + 1)n − w(w + 1)

cells. Store the band as a (2w + 1) × n array, one row per diagonal, and A(i, j) is in row w + j − i and column j: each column of A keeps its column in the array. A few cells at the ends of the side rows go unused. (The program’s three strips are a variant: up[i] holds A(i, i + 1), indexed by row, so its cells sit one place to the left of column j.)

Solving with the strips

The point of the strips is not only the memory. A system Ax = b with a tridiagonal A is solved without ever touching the zeros. The program solves it for b = (0, 0, 0, 0, 6), which is A times (1, 2, 3, 4, 5):

void solve(const double *b, double *x)
{
    double c[N], d[N];
    c[0] = up[0] / di[0];
    d[0] = b[0] / di[0];
    for (int i = 1; i < N; i++) {
        double m = di[i] - lo[i - 1] * c[i - 1];
        if (i < N - 1)
            c[i] = up[i] / m;
        d[i] = (b[i] - lo[i - 1] * d[i - 1]) / m;
    }
    x[N - 1] = d[N - 1];
    for (int i = N - 2; i >= 0; i--)
        x[i] = d[i] - c[i] * x[i + 1];
}

This is the tridiagonal algorithm, also called the Thomas algorithm after the physicist who described it. The sweep down removes the lower diagonal one row at a time. Row i subtracts lo[i − 1] times the row above, which leaves m on the diagonal, and divides by m, so that each row reads x[i] + c[i] x[i + 1] = d[i]. The pass up starts at the last row, which now reads x[4] = d[4], and substitutes each answer into the row above. Replay it here:

Solving with three strips

The sweep down the rows, then the answers from the bottom up

void solve(const double *b, double *x){    double c[N], d[N];    c[0] = up[0] / di[0];    d[0] = b[0] / di[0];    for (int i = 1; i < N; i++) {        double m = di[i] - lo[i - 1] * c[i - 1];        if (i < N - 1)            c[i] = up[i] / m;        d[i] = (b[i] - lo[i - 1] * d[i - 1]) / m;    }    x[N - 1] = d[N - 1];    for (int i = N - 2; i >= 0; i--)        x[i] = d[i] - c[i] * x[i + 1];}    const double b[N] = { 0, 0, 0, 0, 6 };    double x[N];    solve(b, x);    printf("x =");    for (int i = 0; i < N; i++)        printf(" %.6g", x[i]);    printf("\n");    return 0;}

b is A times (1, 2, 3, 4, 5), so the answer should come out as 1, 2, 3, 4, 5.

Output

 2 -1  0  0  0 
-1  2 -1  0  0 
 0 -1  2 -1  0 
 0  0 -1  2 -1 
 0  0  0 -1  2 
stored 13 of 25 cells
0.00 / 21.20 s
Describe this picture

The picture is the same 5 × 5 grid and three strips, with three more strips under them, all of 5 boxes: c, d and x. c and d belong to solve and appear when it starts; x belongs to main and starts empty. Under the picture are the lines of solve and the end of main, with the line about to run marked, one sentence on the current step, and the output. The replay is about 21 seconds. The sweep lights the band cells of row 0, then row 1, and so on down: each row writes one box of c and one of d. The boxes of c fill with −0.5, −0.667, −0.75 and −0.8; its last box is never written and stays unknown. The boxes of d fill with 0, 0, 0, 0 and 5. Then the pass up lights the diagonal cell of row 4, then rows 3, 2, 1 and 0, and x fills from its last box to its first: 5, 4, 3, 2 and 1. The replay rests on the output x = 1 2 3 4 5. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as sweep.step.

The values on the diagonal after elimination, m, are 2, 1.5, 1.333…, 1.25 and 1.2. c is −0.5, −0.667, −0.75 and −0.8, and d is 0, 0, 0, 0 and 5. The pass up gives x[4] = 5, then 4, 3, 2 and 1, and the program prints

x = 1 2 3 4 5

In floating point x[0] comes out as 0.9999999999999998, which %.6g prints as 1. The solver needs no row swaps here because each diagonal entry, 2, is at least as large as its two neighbours together, and larger in the first and last rows, so m never gets close to 0.

The cost

Storage is 3n − 2 cells instead of n². The sweep does 6 arithmetic operations a row (m takes two, c one, d three) and the pass up 2, so the solver takes about 8n operations. Counted the same way, each multiplication and each subtraction one operation, Gaussian elimination on a full n × n matrix takes about 2n³/3. Solving a linear system does not always cost about n³: with a band, it costs about n.

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic tridiag.c -o tridiag
./tridiag

Worked example

A large system. For n = 1,000 the three strips hold 3 × 1,000 − 2 = 2,998 cells against 1,000,000 for the full grid. The solver takes about 8,000 operations; elimination on the full matrix takes about 2 × 1,000³ / 3, roughly 667,000,000, counted the same way.

A band of half-width 2. For n = 5 and w = 2 the band holds (2 × 2 + 1) × 5 − 2 × 3 = 25 − 6 = 19 cells: the main diagonal’s 5, two diagonals of 4 and two of 3.

Two reads. get(3, 2) is lo[2] = −1, since row 3 is one below column 2. get(0, 3) is 0: column 3 is three away from row 0.

Two checks. Which array holds the entry in row 2, column 1? (lo, cell 1.) How many cells does a pentadiagonal (w = 2) 100 × 100 matrix need? (5 × 100 − 6 = 494.)

Where it’s used

Heat and vibration along a rod. Split a rod into points; each point’s temperature or displacement depends only on its two neighbours, so the equations form a tridiagonal system.

Cubic splines. Fitting a smooth curve through points with cubic pieces gives one equation per point, linking it to its neighbours: a tridiagonal system.

Implicit time steps. Simulations that step a diffusion equation implicitly solve a tridiagonal or banded system at every step, which takes about n operations each time. The same elimination, written for any matrix, is LU decomposition.

Reference card

Code or ideaMeaning
lo[N - 1], di[N], up[N - 1]the three diagonals of a tridiagonal matrix
3n − 2cells of a tridiagonal matrix
get(i, j)di[i] if i = j, lo[j] if i = j + 1, up[i] if j = i + 1, else 0
(2w + 1)n − w(w + 1)cells of a band of half-width w
sweep down, pass upthe tridiagonal solver, about 8n operations

Back to diagonal matrices, the band with w = 0. Next, Toeplitz matrices need only one number per diagonal.

Platform: C11, gcc on x86-64 Linux: int 4 bytes, double 8, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look