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
Output
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 idea | Meaning |
|---|---|
| n(n + 1)/2 | entries on and below the diagonal |
i * (i + 1) / 2 + j | row-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_get | swaps 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.