DEV Community

Cover image for Matrix Chain Multiplication: Making the Skeleton Concrete
Nishant Gaurav
Nishant Gaurav

Posted on

Matrix Chain Multiplication: Making the Skeleton Concrete

Chapter 1 introduced the MCM skeleton. This chapter fills in the one line that was left abstract: tempAns. For Matrix Chain Multiplication specifically, that line becomes the cost of multiplying two groups of matrices together after splitting the chain at position k.

Everything else — the i, j boundaries, the k loop, the recursion into both halves, the min aggregation — comes directly from Chapter 1.


The Problem

You're given an array of dimensions representing a chain of matrices.

arr = [40, 20, 30, 10, 30]

Matrix A1: 40 × 20   (arr[0] × arr[1])
Matrix A2: 20 × 30   (arr[1] × arr[2])
Matrix A3: 30 × 10   (arr[2] × arr[3])
Matrix A4: 10 × 30   (arr[3] × arr[4])
Enter fullscreen mode Exit fullscreen mode

The matrices must be multiplied in order — you can't rearrange them. But you can choose where to put the parentheses, and different parenthesizations have different costs.

(A1 × A2) × (A3 × A4)
A1 × (A2 × (A3 × A4))
(A1 × (A2 × A3)) × A4
Enter fullscreen mode Exit fullscreen mode

All three produce the same result matrix. But the number of scalar multiplications required is different for each. Find the parenthesization with the minimum cost.


How Matrix Multiplication Cost Works

To multiply a matrix of dimensions a × b by a matrix of dimensions b × c, you perform a × b × c scalar multiplications, and the result is a × c.

A = 40 × 20
B = 20 × 30

A × B costs: 40 × 20 × 30 = 24,000
Result: 40 × 30
Enter fullscreen mode Exit fullscreen mode

This formula — arr[i-1] × arr[k] × arr[j] — becomes the combining cost in the MCM recurrence.


How the Array Represents the Matrices

This indexing is important and trips people up.

Matrix Ai has dimensions: arr[i-1] × arr[i]

A1 = arr[0] × arr[1]
A2 = arr[1] × arr[2]
A3 = arr[2] × arr[3]
A4 = arr[3] × arr[4]
Enter fullscreen mode Exit fullscreen mode

If arr has n elements, there are n-1 matrices. For arr = [40, 20, 30, 10, 30] with n = 5, there are 4 matrices.

This is why the initial call is solve(1, n-1) not solve(0, n-1). The matrices are indexed 1 through n-1, not 0 through n-1.


Applying the MCM Skeleton

From Chapter 1, the skeleton is:

def solve(i, j):
    if i >= j: return 0
    ans = INF
    for k in range(i, j):
        left    = solve(i, k)
        right   = solve(k + 1, j)
        tempAns = left + right + cost(i, k, j)   ← fill this in
        ans = min(ans, tempAns)
    return ans
Enter fullscreen mode Exit fullscreen mode

For Matrix Chain Multiplication, cost(i, k, j) is the cost of multiplying the result of the left group with the result of the right group.

After solve(i, k), the left group produces a matrix of dimensions arr[i-1] × arr[k].
After solve(k+1, j), the right group produces a matrix of dimensions arr[k] × arr[j].

Multiplying these two together costs: arr[i-1] × arr[k] × arr[j].

So:

tempAns = solve(i, k) + solve(k + 1, j) + arr[i-1] * arr[k] * arr[j]
Enter fullscreen mode Exit fullscreen mode

That's the only line that needed to be determined. The rest was already in the skeleton.


The Full Recurrence

solve(i, j) = minimum cost to multiply matrices Ai through Aj

Base case:
  if i >= j: return 0   (one matrix or less — no multiplication needed)

Transition:
  for k in range(i, j):
    tempAns = solve(i, k) + solve(k + 1, j) + arr[i-1] * arr[k] * arr[j]
    ans = min(ans, tempAns)
Enter fullscreen mode Exit fullscreen mode

The Recursive Solution

class Solution:

    def solve(self, arr, i, j):

        # Base case: one matrix or invalid range — no cost
        if i >= j:
            return 0

        ans = float('inf')

        # Try every possible split point
        for k in range(i, j):

            # Cost of left group + cost of right group + cost of combining them
            tempAns = (
                self.solve(arr, i, k)
                + self.solve(arr, k + 1, j)
                + arr[i - 1] * arr[k] * arr[j]
            )

            ans = min(ans, tempAns)

        return ans

    def matrixMultiplication(self, arr):
        n = len(arr)
        return self.solve(arr, 1, n - 1)
Enter fullscreen mode Exit fullscreen mode

This works correctly but is slow. The same (i, j) pair appears in multiple branches of the recursion. For example, solve(2, 3) might be computed once when k=1 calls solve(2, 4) which calls solve(2, 3), and again from a different path. Memoization fixes this.


Adding Memoization

The state is (i, j). The table is (n × n) initialised to -1. The k loop is not part of the state — it's just how we explore all options for a given (i, j).

class Solution:

    def matrixMultiplication(self, arr):

        n = len(arr)

        # -1 = not computed yet (0 is a valid cost, so can't use 0)
        T = [[-1] * n for _ in range(n)]

        def solve(i, j):

            # Base case
            if i >= j:
                return 0

            # Return stored answer if already computed
            if T[i][j] != -1:
                return T[i][j]

            ans = float('inf')

            for k in range(i, j):

                tempAns = (
                    solve(i, k)
                    + solve(k + 1, j)
                    + arr[i - 1] * arr[k] * arr[j]
                )

                ans = min(ans, tempAns)

            T[i][j] = ans
            return ans

        return solve(1, n - 1)
Enter fullscreen mode Exit fullscreen mode

The memoization check (if T[i][j] != -1) is placed before the loop. If (i, j) was already solved, return the stored answer immediately without re-running the loop.

Note: -1 is the sentinel because 0 is a valid answer (a single matrix has zero multiplication cost). The same reasoning applies here as it did in Coin Change I and Count of Subsets from the Knapsack series.


A Partial Dry Run

arr = [40, 20, 30, 10, 30]
Call: solve(1, 4)

k = 1: A1 | A2 A3 A4
  left  = solve(1, 1) = 0
  right = solve(2, 4) = ...
  cost  = arr[0] * arr[1] * arr[4] = 40 * 20 * 30 = 24,000

k = 2: A1 A2 | A3 A4
  left  = solve(1, 2) = ...
  right = solve(3, 4) = ...
  cost  = arr[0] * arr[2] * arr[4] = 40 * 30 * 30 = 36,000

k = 3: A1 A2 A3 | A4
  left  = solve(1, 3) = ...
  right = solve(4, 4) = 0
  cost  = arr[0] * arr[3] * arr[4] = 40 * 10 * 30 = 12,000
Enter fullscreen mode Exit fullscreen mode

The final answer is min of tempAns across all three values of k. The recursive calls compute the internal costs. The answer for arr = [40, 20, 30, 10, 30] is 26,000.


MCM Pattern Checklist for This Problem

Question Answer
What is i? Left matrix index (1-based)
What is j? Right matrix index (1-based)
Base case? if i >= j: return 0
k range? range(i, j)
Left subproblem? solve(i, k)
Right subproblem? solve(k+1, j)
Combining cost? arr[i-1] * arr[k] * arr[j]
Aggregation? min

Quick Revision

arr = dimensions array, n = len(arr), n-1 matrices
Matrix Ai = arr[i-1] × arr[i]
Initial call: solve(1, n-1)

Base case: if i >= j: return 0

Transition:
  for k in range(i, j):
    tempAns = solve(i,k) + solve(k+1,j) + arr[i-1]*arr[k]*arr[j]
    ans = min(ans, tempAns)

Memoization: T[i][j], initialised to -1
  Why -1: 0 is a valid answer (single matrix costs 0)
Enter fullscreen mode Exit fullscreen mode

What You Now Understand

The MCM skeleton from Chapter 1 applied directly. The only new work was identifying that the combining cost is arr[i-1] * arr[k] * arr[j] — the cost of multiplying the result of the left group with the result of the right group. Everything else was already written.

The next chapter applies the same skeleton to Palindrome Partitioning: given a string, find the minimum number of cuts to partition it into palindromes. The tempAns changes, the base case changes slightly, and the aggregation stays min. The structure doesn't change at all.

Top comments (0)