Prerequisites
Before attempting this problem, you should be comfortable with:
- Matrix Multiplication - Understanding how to compute the dot product of rows and columns
- Sparse Matrix Concepts - Recognizing that sparse matrices have mostly zero elements
- Two-Pointer Technique - Merging or comparing two sorted sequences efficiently
- Compressed Sparse Formats - Understanding CSR (Compressed Sparse Row) and CSC (Compressed Sparse Column) representations
1. Naive Iteration
Intuition
Standard matrix multiplication computes each element of the result by taking the dot product of a row from the first matrix and a column from the second. For sparse matrices (matrices with many zeros), we can skip computations involving zero elements since they contribute nothing to the result. By checking if an element in mat1 is non-zero before processing, we avoid unnecessary multiplications.
Algorithm
- Create a result matrix of size
m x ninitialized with zeros. - Iterate through each row of
mat1. - For each element in the row, check if it is non-zero.
- If non-zero, multiply it with each element in the corresponding row of
mat2and add to the appropriate position in the result. - Return the result matrix.
class Solution:
def multiply(self, mat1: List[List[int]], mat2: List[List[int]]) -> List[List[int]]:
# Product matrix.
ans = [[0] * len(mat2[0]) for _ in range(len(mat1))]
for row_index, row_elements in enumerate(mat1):
for element_index, row_element in enumerate(row_elements):
# If current element of mat1 is non-zero then iterate over all columns of mat2.
if row_element:
for col_index, col_element in enumerate(mat2[element_index]):
ans[row_index][col_index] += row_element * col_element
return ansTime & Space Complexity
- Time complexity:
- Space complexity:
Where and represent the number of rows and columns in
mat1, respectively and and represent the number of rows and columns inmat2, respectively.
2. List of Lists
Intuition
To further optimize sparse matrix multiplication, we can preprocess both matrices to store only their non-zero elements. For each row, we keep a list of (value, column) pairs representing non-zero entries. This compressed representation allows us to iterate only over non-zero elements when computing the product, making the algorithm efficient for highly sparse matrices.
Algorithm
- Compress both matrices by storing only non-zero elements. For each row, create a list of
(value, column index)pairs. - Create a result matrix of size
m x ninitialized with zeros. - For each row in
mat1, iterate through its non-zero elements. - For each non-zero element at column
cinmat1, look at rowcof the compressedmat2. - Multiply the
mat1element with each non-zero element in that row ofmat2and add to the result. - Return the result matrix.
class Solution:
def multiply(self, mat1: List[List[int]], mat2: List[List[int]]) -> List[List[int]]:
def compress_matrix(matrix: List[List[int]]) -> List[List[int]]:
rows, cols = len(matrix), len(matrix[0])
compressed_matrix = [[] for _ in range(rows)]
for row in range(rows):
for col in range(cols):
if matrix[row][col]:
compressed_matrix[row].append([matrix[row][col], col])
return compressed_matrix
m = len(mat1)
k = len(mat1[0])
n = len(mat2[0])
# Store the non-zero values of each matrix.
A = compress_matrix(mat1)
B = compress_matrix(mat2)
ans = [[0] * n for _ in range(m)]
for mat1_row in range(m):
# Iterate on all current 'row' non-zero elements of mat1.
for element1, mat1_col in A[mat1_row]:
# Multiply and add all non-zero elements of mat2
# where the row is equal to col of current element of mat1.
for element2, mat2_col in B[mat1_col]:
ans[mat1_row][mat2_col] += element1 * element2
return ansTime & Space Complexity
- Time complexity:
- Space complexity:
Where and represent the number of rows and columns in
mat1, respectively and and represent the number of rows and columns inmat2, respectively.
3. Yale Format
Intuition
The Yale format (also known as Compressed Sparse Row/Column) is a standard way to represent sparse matrices efficiently. It uses three arrays: values (non-zero elements), column/row indices, and row/column pointers that mark where each row/column starts in the values array. By compressing mat1 row-wise and mat2 column-wise, we can efficiently compute dot products by merging two sorted lists of indices.
Algorithm
- Compress
mat1using Compressed Sparse Row (CSR) format: store values, column indices, and row pointers. - Compress
mat2using Compressed Sparse Column (CSC) format: store values, row indices, and column pointers. - For each cell
(row, col)in the result matrix, find the range of non-zero elements inmat1's row andmat2's column. - Use a two-pointer technique to merge these ranges: when column index in
mat1matches row index inmat2, multiply the values and add to the result. - Return the result matrix.
class SparseMatrix:
def __init__(self, matrix: List[List[int]], col_wise: bool):
self.values, self.row_index, self.col_index = self.compress_matrix(matrix, col_wise)
def compress_matrix(self, matrix: List[List[int]], col_wise: bool):
return self.compress_col_wise(matrix) if col_wise else self.compress_row_wise(matrix)
# Compressed Sparse Row
def compress_row_wise(self, matrix: List[List[int]]):
values = []
row_index = [0]
col_index = []
for row in range(len(matrix)):
for col in range(len(matrix[0])):
if matrix[row][col]:
values.append(matrix[row][col])
col_index.append(col)
row_index.append(len(values))
return values, row_index, col_index
# Compressed Sparse Column
def compress_col_wise(self, matrix: List[List[int]]):
values = []
row_index = []
col_index = [0]
for col in range(len(matrix[0])):
for row in range(len(matrix)):
if matrix[row][col]:
values.append(matrix[row][col])
row_index.append(row)
col_index.append(len(values))
return values, row_index, col_index
class Solution:
def multiply(self, mat1: List[List[int]], mat2: List[List[int]]) -> List[List[int]]:
A = SparseMatrix(mat1, False)
B = SparseMatrix(mat2, True)
ans = [[0] * len(mat2[0]) for _ in range(len(mat1))]
for row in range(len(ans)):
for col in range(len(ans[0])):
# Row element range indices
mat1_row_start = A.row_index[row]
mat1_row_end = A.row_index[row + 1]
# Column element range indices
mat2_col_start = B.col_index[col]
mat2_col_end = B.col_index[col + 1]
# Iterate over both row and column.
while mat1_row_start < mat1_row_end and mat2_col_start < mat2_col_end:
if A.col_index[mat1_row_start] < B.row_index[mat2_col_start]:
mat1_row_start += 1
elif A.col_index[mat1_row_start] > B.row_index[mat2_col_start]:
mat2_col_start += 1
# Row index and col index are same so we can multiply these elements.
else:
ans[row][col] += A.values[mat1_row_start] * B.values[mat2_col_start]
mat1_row_start += 1
mat2_col_start += 1
return ansTime & Space Complexity
- Time complexity:
- Space complexity:
Where and represent the number of rows and columns in
mat1, respectively and and represent the number of rows and columns inmat2, respectively.
Common Pitfalls
Not Skipping Zero Elements in the First Matrix
The main optimization for sparse matrices is to skip multiplications when an element in mat1 is zero. Checking only for zeros in mat2 or not checking at all results in unnecessary computations and misses the performance benefit of sparse matrix multiplication.
Confusing Row and Column Indices During Multiplication
In matrix multiplication, element mat1[i][k] multiplies with mat2[k][j] to contribute to result[i][j]. Mixing up the shared index k or the result indices i and j leads to incorrect results.
Initializing the Result Matrix Incorrectly
The result matrix must be initialized with zeros and have dimensions m x n where m is the number of rows in mat1 and n is the number of columns in mat2. Using wrong dimensions or forgetting to initialize with zeros causes index errors or incorrect sums.