129. Sparse Matrix Multiplication
Problem
Multiply compatible rectangular integer matrices A of shape m-by-k and B of shape k-by-n, returning a dense m-by-n result. Skip zero contributions. Empty A returns []; when the shared dimension and B are empty, return m empty rows. Inputs are unchanged.
Examples
Example 1
sparse_multiply([[1,0,2]], [[1,2],[9,9],[3,4]])
=> [[7, 10]]The zero coefficient skips the middle row of B.
Example 2
sparse_multiply([[0,0]], [[1],[2]])
=> [[0]]All product contributions are zero.
Example 3
sparse_multiply([], [])
=> []There are no result rows.
Reveal the worked solution and reasoning
Why this mechanism works
A[i,p] contributes only through nonzero entries of B's row p. Pre-index those entries, then skip zero A values entirely. This retains the ordinary matrix-product formula while avoiding multiplications known to contribute zero.
State and boundaries
nonzero[p] lists (column,value) entries in B row p. output[i][j] accumulates contributions from processed shared-dimension indices.
Protocol
- Index nonzero entries of each B row.
- Allocate the dense output.
- For each nonzero A[i,p], visit only indexed nonzeros of B[p].
- Add A[i,p]*B[p,j] to output[i,j].
Visualization
A row [1,0,2]
use B row 0 and twice B row 2
B row 1 contributes nothing for this output rowPython implementation
def sparse_multiply(a, b):
rows = len(a)
cols = len(b[0]) if b else 0
nonzero = [[(j, value) for j, value in enumerate(row) if value != 0]
for row in b]
output = [[0] * cols for _ in range(rows)]
for i, row in enumerate(a):
for p, value in enumerate(row):
if value != 0:
for j, other in nonzero[p]:
output[i][j] += value * other
return output
Worked execution
sparse_multiply([[1,0,2]], [[1,2],[9,9],[3,4]])
L2 sparse_multiply: rows = len(a) a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1 L3 sparse_multiply: cols = len(b[0]) if b else 0 a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2 L5 sparse_multiply: for row in b] a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; row=[1, 2] L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; row=[1, 2]; j=0; value=1 L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; row=[1, 2]; j=1; value=2 L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; row=[1, 2] L5 sparse_multiply: for row in b] a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; row=[9, 9] L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; row=[9, 9]; j=0; value=9 ... 17 intermediate state changes omitted ... L10 sparse_multiply: for j, other in nonzero[p]: a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; nonzero=[[(..., ...), (..., ...)], [(..., ...), (..., ...)], [(..., ...), (..., ...)]]; output=[[1, 2]]; row=[1, 0, 2]; i=0; value=2; p=2; j=0; other=3 L11 sparse_multiply: output[i][j] += value * other a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; nonzero=[[(..., ...), (..., ...)], [(..., ...), (..., ...)], [(..., ...), (..., ...)]]; output=[[7, 2]]; row=[1, 0, 2]; i=0; value=2; p=2; j=0; other=3 L10 sparse_multiply: for j, other in nonzero[p]: a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; nonzero=[[(..., ...), (..., ...)], [(..., ...), (..., ...)], [(..., ...), (..., ...)]]; output=[[7, 2]]; row=[1, 0, 2]; i=0; value=2; p=2; j=1; other=4 L11 sparse_multiply: output[i][j] += value * other a=[[1, 0, 2]]; b=[[1, 2], [9, 9], [3, 4]]; rows=1; cols=2; nonzero=[[(..., ...), (..., ...)], [(..., ...), (..., ...)], [(..., ...), (..., ...)]]; output=[[7, 10]]; row=[1, 0, 2]; i=0; value=2; p=2; j=1; other=4
Result: [[7, 10]]. The zero coefficient skips the middle row of B.
sparse_multiply([[0,0]], [[1],[2]])
L2 sparse_multiply: rows = len(a) a=[[0, 0]]; b=[[1], [2]]; rows=1 L3 sparse_multiply: cols = len(b[0]) if b else 0 a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1 L5 sparse_multiply: for row in b] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; row=[1] L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; row=[1]; j=0; value=1 L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; row=[1] L5 sparse_multiply: for row in b] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; row=[2] L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; row=[2]; j=0; value=2 L4 sparse_multiply: nonzero = [[(j, value) for j, value in enumerate(row) if value != 0] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; row=[2] ... 2 intermediate state changes omitted ... L6 sparse_multiply: output = [[0] * cols for _ in range(rows)] a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; nonzero=[[(..., ...)], [(..., ...)]]; output=[[0]] L7 sparse_multiply: for i, row in enumerate(a): a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; nonzero=[[(..., ...)], [(..., ...)]]; output=[[0]]; row=[0, 0]; i=0 L8 sparse_multiply: for p, value in enumerate(row): a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; nonzero=[[(..., ...)], [(..., ...)]]; output=[[0]]; row=[0, 0]; i=0; value=0; p=0 L8 sparse_multiply: for p, value in enumerate(row): a=[[0, 0]]; b=[[1], [2]]; rows=1; cols=1; nonzero=[[(..., ...)], [(..., ...)]]; output=[[0]]; row=[0, 0]; i=0; value=0; p=1
Result: [[0]]. All product contributions are zero.
Why the solution is correct
The product definition sums A[i,p]*B[p,j] over every shared index p. Skipped terms have a zero factor and contribute nothing; all nonzero-factor terms are added exactly once to the correct output coordinate. Thus sparsity changes work, not the result.
Time and space
O(kn+mk+mn+W) time, where W is the number of visited nonzero product pairs; O(nnz(B)+mn) space. Worst-case time remains O(mkn) for dense inputs.
Common mistakes
Skipping an entire output column because one A entry is zero loses other contributions. Dense output allocation still costs O(mn), even with sparse inputs.
Reasoning card
State: nonzero[p] lists (column,value) entries in B row p. output[i][j] accumulates contributions from processed shared-dimension indices.
Invariant: Accumulated output entries include all processed inner-dimension contributions.
Decision to rehearse: Iterate only meaningful nonzero products.
Finish: Add A[i,p]*B[p,j] to output[i,j]
Transfer exercise
Represent the output sparsely too, and distinguish multiplication work from the cost of materializing a dense result.