Skip to content

Triangular and symmetric matrices

A triangular matrix fits in n(n + 1)/2 cells, with a formula that maps each entry to its place, and a symmetric matrix is stored as one of its triangles.

Helps to know firstDiagonal matrices

Helps to know first
This topic

First, the picture

The 15 entries on and below the diagonal of a 5 × 5 matrix, packed into one strip. Watch each row land where the rows before it end.

Packing a triangle into a line

Fifteen entries of a 5 × 5 triangle in one strip

static int store[CELLS]; int index_of(int i, int j){#if COLUMN    return j * N - j * (j - 1) / 2 + (i - j);#else    return i * (i + 1) / 2 + j;#endif} int get(int i, int j){    return j <= i ? store[index_of(i, j)] : 0;} int sym_get(int i, int j){    if (j > i) {        int t = i;        i = j;        j = t;    }    return store[index_of(i, j)];} int main(void){    for (int i = 0; i < N; i++)        for (int j = 0; j <= i; j++)            store[index_of(i, j)] = 10 * (i + 1) + (j + 1);    for (int i = 0; i < N; i++) {        for (int j = 0; j < N; j++)            printf("%2d ", get(i, j));        printf("\n");    }    printf("store:");    for (int k = 0; k < CELLS; k++)        printf(" %d", store[k]);    printf("\nL(4, 2) = %d at index %d; L(2, 4) = %d; S(2, 4) = %d\n",           get(4, 2), index_of(4, 2), get(2, 4), sym_get(2, 4));    return 0;}

Pack the lower triangle into store[15], row after row.

cells stored
15 of 25
Storage order

Output

0.00 / 14.80 s
Describe this picture

The picture is a 5 × 5 grid with the row and column numbers beside it, and under it a strip of 15 boxes, store. Each entry on or below the diagonal is labelled by its position, 10(i + 1) + (j + 1), so entry (4, 2) is 53; the 10 cells above the diagonal 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 strip. Under the grid is a line of text with the formula for the current row or cell. Under the picture are the program tri.c, with the line about to run marked, one sentence on the current step, a readout “cells stored” reading “15 of 25”, the control, and the output. The clip plays once, about 15 seconds. Row-major, the rows fly down one at a time: row 0’s single entry into box 0, row 1’s two entries into boxes 1 and 2, row 2’s three into boxes 3 to 5, row 3’s four into 6 to 9, and row 4’s five into 10 to 14; the line under the grid says where each row starts, 0, 1, 3, 6, 10. The program prints the matrix through get and then the store. Next the grid becomes the symmetric matrix, its upper cells showing their mirrors’ values, and cells (2, 4) and (4, 2) light together on one box, 12. Then get(2, 4) returns 0, and the index of L(4, 2) is computed: 12. The clip rests on the output. The control, “Storage order”, switches between “Row-major” and “Column-major”; column-major, the same rows fly into scattered boxes (row 1 into boxes 1 and 5, row 2 into 2, 6 and 9), and L(4, 2) and its mirror share box 11. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as triangle.step, and the order as triangle.order.

Half a grid in one strip

A lower triangular matrix is zero above its diagonal: L(i, j) = 0 whenever j > i. Row 0 has 1 entry that can be non-zero, row 1 has 2, and row n − 1 has n. So the triangle holds

1 + 2 + … + n = n(n + 1)/2

entries. For n = 5 that is 15 of 25. An upper triangular matrix is the mirror image, zero below its diagonal, with the same count.

The program keeps the 15 entries of a 5 × 5 lower triangle in one array, static int store[15], and needs a rule that says which cell holds L(i, j). Each entry’s value is its position, 10(i + 1) + (j + 1), so you can see in the output which entry landed in which cell.

The code and its cost

Row after row

Store the rows one after another. Rows 0 to i − 1 hold 1 + 2 + … + i = i(i + 1)/2 entries, so row i starts at that cell, and L(i, j) is j cells further on:

int index_of(int i, int j)
{
    return i * (i + 1) / 2 + j;
}

The runs start at 0, 1, 3, 6 and 10, the triangular numbers. L(4, 2) is at 4 × 5 / 2 + 2 = 12. The program prints the store in order:

store: 11 21 22 31 32 33 41 42 43 44 51 52 53 54 55
L(4, 2) = 53 at index 12; L(2, 4) = 0; S(2, 4) = 53

Column after column

Store the columns instead. Column 0 holds n entries, column 1 holds n − 1, and so on, so columns 0 to j − 1 hold n + (n − 1) + … + (n − j + 1) = jn − j(j − 1)/2 entries. L(i, j) is i − j cells into column j:

return j * N - j * (j - 1) / 2 + (i - j);

Now the runs are the columns, 5, 4, 3, 2 and 1 long. The program loops over the rows exactly as before; only index_of changes, so each row’s entries land in scattered cells. Set the control to “Column-major” to watch it. The same 15 values end up in another order:

store: 11 21 31 41 51 22 32 42 52 33 43 53 44 54 55
L(4, 2) = 53 at index 11; …

The two formulas give different cells for the same entry: 12 against 11 for L(4, 2). A program must read with the formula it wrote with.

Reading an entry

get supplies the zeros above the diagonal, as the diagonal matrix’s get did:

int get(int i, int j)
{
    return j <= i ? store[index_of(i, j)] : 0;
}

The program prints all 25 entries through get, rows 11 0 0 0 0 to 51 52 53 54 55.

Upper triangles

An upper triangle is the transpose of a lower one: U(i, j) = L(j, i). Store it by swapping the roles of i and j. Row-major, row i of an upper triangle holds n − i entries, so it starts at in − i(i − 1)/2 and U(i, j) is at in − i(i − 1)/2 + (j − i).

Symmetric matrices

A symmetric matrix has M(i, j) = M(j, i). Its upper half repeats its lower half, so store only the lower triangle and swap i and j when asked for an entry above the diagonal:

int sym_get(int i, int j)
{
    if (j > i) {
        int t = i;
        i = j;
        j = t;
    }
    return store[index_of(i, j)];
}

S(2, 4) becomes S(4, 2), and both read cell 12: the clip lights them together. The program prints S(2, 4) = 53, while the triangle itself has L(2, 4) = 0.

The cost

Storage drops from n² to n(n + 1)/2, about half. Reading an entry is one formula and one array read, as fast as a full grid.

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic tri.c -o tri
./tri
gcc -std=c11 -Wall -Wextra -pedantic -DCOLUMN=1 tri.c -o tri-col
./tri-col

Worked example

Two formulas, one entry. L(3, 1): row-major, 3 × 4 / 2 + 1 = 7; column-major, 1 × 5 − 0 + 2 = 7, the same by chance. L(4, 2): row-major 12, column-major 2 × 5 − 1 + 2 = 11.

A large triangle. For n = 1,000 the triangle holds 1,000 × 1,001 / 2 = 500,500 cells, against 1,000,000 for the full grid.

Distances between cities. A table of distances between 5 cities is symmetric, since the road from A to B is as long as the road from B to A, and its diagonal is 0. The entries strictly below the diagonal, 4 + 3 + 2 + 1 = 10 numbers, hold the whole table.

Two checks. Where does row 3 start in row-major order? (Cell 6.) How many cells does a symmetric 10 × 10 matrix need? (55.)

Where it’s used

Distance tables. Road atlases and route planners keep one triangle of a symmetric table.

Covariance matrices. In statistics the covariance of a set of variables is symmetric, so libraries store one triangle.

Structures. The stiffness matrix of a bridge or frame in an engineering simulation is symmetric.

Solving equations. Linear solvers factor a matrix into a lower and an upper triangle (LU decomposition); symmetric matrices have special factorisations of their own.

Reference card

Code or ideaMeaning
n(n + 1)/2entries on and below the diagonal
i * (i + 1) / 2 + jrow-major index of L(i, j), j ≤ i
j * N - j * (j - 1) / 2 + (i - j)column-major index of L(i, j)
U(i, j) = L(j, i)an upper triangle is a transposed lower one
sym_getswaps i and j above the diagonal: M(i, j) = M(j, i)

Next, banded matrices keep only a few diagonals.

Platform: C11, gcc on x86-64 Linux: int 4 bytes, 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