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

  1. Create a result matrix of size m x n initialized with zeros.
  2. Iterate through each row of mat1.
  3. For each element in the row, check if it is non-zero.
  4. If non-zero, multiply it with each element in the corresponding row of mat2 and add to the appropriate position in the result.
  5. 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 ans

Time & Space Complexity

  • Time complexity: O(mkn)O(m \cdot k \cdot n)
  • Space complexity: O(1)O(1)

Where mm and kk represent the number of rows and columns in mat1, respectively and kk and nn represent the number of rows and columns in mat2, 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

  1. Compress both matrices by storing only non-zero elements. For each row, create a list of (value, column index) pairs.
  2. Create a result matrix of size m x n initialized with zeros.
  3. For each row in mat1, iterate through its non-zero elements.
  4. For each non-zero element at column c in mat1, look at row c of the compressed mat2.
  5. Multiply the mat1 element with each non-zero element in that row of mat2 and add to the result.
  6. 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 ans

Time & Space Complexity

  • Time complexity: O(mkn)O(m \cdot k \cdot n)
  • Space complexity: O(mk+kn)O(m \cdot k + k \cdot n)

Where mm and kk represent the number of rows and columns in mat1, respectively and kk and nn represent the number of rows and columns in mat2, 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

  1. Compress mat1 using Compressed Sparse Row (CSR) format: store values, column indices, and row pointers.
  2. Compress mat2 using Compressed Sparse Column (CSC) format: store values, row indices, and column pointers.
  3. For each cell (row, col) in the result matrix, find the range of non-zero elements in mat1's row and mat2's column.
  4. Use a two-pointer technique to merge these ranges: when column index in mat1 matches row index in mat2, multiply the values and add to the result.
  5. 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 ans

Time & Space Complexity

  • Time complexity: O(mnk)O(m \cdot n \cdot k)
  • Space complexity: O(mk+kn)O(m \cdot k + k \cdot n)

Where mm and kk represent the number of rows and columns in mat1, respectively and kk and nn represent the number of rows and columns in mat2, 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.