Algorithm

Matrix Chain Multiplication

Dynamic Programming Pattern

Matrix Chain Multiplication

You are given an array dims of length n + 1 describing a chain of n matrices, where matrix i has dims[i-1] rows and dims[i] columns for i from 1 to n. Multiplying a p × q matrix by a q × r matrix costs p × q × r scalar multiplications and produces a p × r matrix. Matrix multiplication is associative, so you may choose any order of pairings. Return the minimum total number of scalar multiplications needed to compute the whole product.

CONSTRAINTS
  • 2 ≤ dims.length ≤ 100, so there are at most 99 matrices
  • 1 ≤ dims[i] ≤ 100
  • The order of the matrices in the chain is fixed and may not be rearranged
  • Only the grouping — where the parentheses go — is yours to choose
EXAMPLE 1
Input: dims = [1, 2, 3, 4]
Output: 18
Grouping as (AB)C costs 6 for the first product plus 12 for the second. Grouping as A(BC) costs 24 plus 8, which is 32 — the same result computed with nearly twice the work.
EXAMPLE 2
Input: dims = [10, 20, 30]
Output: 6000
Two matrices leave no choice of grouping at all, so the cost is the single product 10 × 20 × 30.
EXAMPLE 3
Input: dims = [40, 20, 30, 10, 30]
Output: 26000
The best grouping first collapses the middle matrices into a thin 20 × 10 result, so the large outer dimensions are only ever joined through a small shared dimension. The worst grouping costs 120000.
EXAMPLE 4
Input: dims = [10, 20, 30, 40, 30]
Output: 30000
The winning arrangement brackets the first three matrices together and multiplies that result by the last, which keeps the costly 40 dimension out of all but one product.
EXAMPLE 5
Input: dims = [5, 6]
Output: 0
A single matrix cannot be multiplied by anything, so no scalar multiplications are performed.
Can I reorder the matrices themselves?
No. Matrix multiplication is associative but not commutative, so the sequence is fixed and only the placement of parentheses is yours to choose. Reordering would change the result, not just the cost.
How do I read the dimensions array?
Matrix i is dims[i-1] rows by dims[i] columns, so an array of n + 1 numbers describes n matrices. The shared entries guarantee that neighbouring matrices always agree in size.
Am I counting scalar multiplications or actual runtime?
Scalar multiplications — the p × q × r count for each product. It is the standard proxy for cost, and it is worth confirming, since a real implementation would also care about memory traffic.
Do I need to output the parenthesisation itself?
Only the cost. To recover the bracketing you would store the winning split point k for each interval and then print the structure recursively from the outermost interval inward.
What is the answer for a chain of one matrix?
Zero, since nothing needs multiplying. That is precisely the base case the table is built on.
Why the grouping changes the cost

Multiplying a p × q matrix by a q × r matrix takes p × q × r scalar multiplications — one for each cell of the p × r result, times the q terms summed into it. The order of the matrices in the chain is fixed and cannot be shuffled, but because matrix multiplication is associative, you may choose where the parentheses go, and different groupings cost wildly different amounts while producing the identical result.

Take three matrices A (1 × 2), B (2 × 3), C (3 × 4).

- (AB)C: AB costs 1 × 2 × 3 = 6 and yields a 1 × 3. Times C costs 1 × 3 × 4 = 12. Total 18.
- A(BC): BC costs 2 × 3 × 4 = 24 and yields a 2 × 4. A times that costs 1 × 2 × 4 = 8. Total 32.

Nearly twice the work for the same answer. On longer chains the gap becomes enormous, which is why compilers and numerical libraries genuinely solve this problem.

The dimensions are given as a single array because adjacent matrices must agree: if matrix i is dims[i-1] × dims[i], then matrix i+1 starts with dims[i] rows automatically. One array of n+1 numbers encodes n matrices with no possibility of a mismatch.

Why the obvious rules fail

"Always multiply the cheapest adjacent pair first" seems reasonable. It is wrong, because the cheap multiplication you perform now leaves behind a result matrix whose shape determines the cost of everything afterwards. A cheap step can leave a fat matrix that poisons the rest of the chain, and an expensive step can leave a thin one that makes everything after it cheap.

Enumerating every parenthesisation is not an option either — the number of ways to bracket n matrices is the Catalan number, which grows roughly like 4ⁿ. For 99 matrices that is beyond astronomical.

The state, as a story

Here is the trick. The other problems walked along one element at a time, but a chain has no natural "next" — the grouping is a tree, not a walk. So instead of the first step, I think about the last one.

Say I want to multiply matrices i through j. However I bracket them, there is exactly one multiplication I do last, and it joins a left block with a right block. That last step splits the chain at some k: matrices i..k are already collapsed into one matrix, matrices k+1..j into another, and I multiply those two.

I don't know the best k, so I try every split from i to j-1, solve each side the same way, and keep the cheapest.

Why can the two sides be solved separately? Because their shapes are fixed no matter how each is bracketed inside: block i..k is dims[i-1] × dims[k], block k+1..j is dims[k] × dims[j]. So the final multiply always costs dims[i-1] × dims[k] × dims[j], and the two sub-costs simply add.

State Definitionf(i, j) is the fewest scalar multiplications to collapse matrices i through j into one matrix.

f(i, j) = min over k from i to j-1 of [ f(i, k) + f(k+1, j) + dims[i-1] × dims[k] × dims[j] ]

Base case: f(i, i) = 0 — a single matrix needs no multiplication. The answer is f(1, n).

From brute force to memo

Write the story down as plain recursion — it simply tries every split:

python
def matrix_chain(dims):
    n = len(dims) - 1              # number of matrices

    def f(i, j):
        if i == j: return 0                       # single matrix: nothing to do
        best = float('inf')
        for k in range(i, j):                     # last multiplication splits after k
            cost = f(i, k) + f(k + 1, j) + dims[i - 1] * dims[k] * dims[j]
            best = min(best, cost)
        return best

    return f(1, n)

Correct, but it re-solves the same intervals again and again — the number of bracketings is Catalan-many, roughly 4ⁿ. Yet there are only about N²/2 different (i, j) intervals. So keep a notebook and solve each once:

python
def matrix_chain(dims):
    n = len(dims) - 1
    memo = {}

    def f(i, j):
        if i == j: return 0
        if (i, j) in memo: return memo[(i, j)]
        best = float('inf')
        for k in range(i, j):
            cost = f(i, k) + f(k + 1, j) + dims[i - 1] * dims[k] * dims[j]
            best = min(best, cost)
        memo[(i, j)] = best
        return best

    return f(1, n)
The table, from the recurrence

Turn it into a table. Let dp[i][j] hold what f(i, j) returned, and swap each call for an array read:

dp[i][j] = min over k of [ dp[i][k] + dp[k+1][j] + dims[i-1] × dims[k] × dims[j] ]

Now the fill order — and this is where interval problems differ. dp[i][j] reads dp[i][k] and dp[k+1][j], both covering shorter stretches of the chain, but one has a larger i and the other a smaller j. Neither index moves consistently, so no plain ascending or descending loop works. What always shrinks, though, is the interval length. So loop by length, shortest first — then every sub-interval a cell needs is already filled. The base case dp[i][i] = 0 is the length-1 diagonal, and the answer is dp[1][n]:

python
def matrix_chain(dims):
    n = len(dims) - 1
    dp = [[0] * (n + 1) for _ in range(n + 1)]     # dp[i][i] = 0 already

    for length in range(2, n + 1):                 # chains of 2, then 3, ...
        for i in range(1, n - length + 2):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = (dp[i][k] + dp[k + 1][j]
                        + dims[i - 1] * dims[k] * dims[j])
                dp[i][j] = min(dp[i][j], cost)

    return dp[1][n]
Crucial Notewhen a recurrence depends on sub-intervals, iterate by interval length, shortest first. Every sub-interval a cell needs is strictly shorter than itself, so by the time you reach length L, all lengths below L are complete. This single rule is what makes every interval-style table work, and it is the reason the earlier advice about ascending or descending index loops does not apply here. There is also no useful space optimisation — the whole triangle of the table is live, because a long interval can need a short one from anywhere inside it.
Worked Example:dims = [1, 2, 3, 4]
Three matrices: A is 1 × 2, B is 2 × 3, C is 3 × 4.

Length 2:
- f(1,2) — only split k = 1: 0 + 0 + (1 × 2 × 3) = 6
- f(2,3) — only split k = 2: 0 + 0 + (2 × 3 × 4) = 24

Length 3:
- f(1,3), two splits to try:
- k = 1, meaning A(BC): f(1,1) + f(2,3) + 1 × 2 × 4 = 0 + 24 + 8 = 32
- k = 2, meaning (AB)C: f(1,2) + f(3,3) + 1 × 3 × 4 = 6 + 0 + 12 = 18

Answer 18, matching the hand calculation at the top of the page. And notice that the winning split reused f(1,2) = 6, computed once and consulted here — the whole reason for the table.

A longer one, dims = [40, 20, 30, 10, 30]: the table works up through lengths 2 and 3 and settles at 26000, against 120000 for the worst bracketing. The best grouping multiplies the middle pair down to a thin 20 × 10 matrix first, so that the expensive outer dimensions are only ever combined through a small shared dimension.

The pattern: interval dynamic programming

This is a different animal from everything earlier in the section, and recognising it is what matters.

The signal: the answer for a stretch of the input is built from the answers for two adjacent sub-stretches, and you get to choose where they meet. The input is a sequence, but the solution is a tree of pairings rather than a walk from one end to the other.

The state: an interval (i, j) — a left and a right boundary — rather than a single position.

The choice: the split point k inside the interval, tried exhaustively.

The loop order: by interval length, shortest first.

The typical cost: O(N³), from N² intervals times N split points.

The step that takes practice is finding the right thing to fix. Here we fixed the last multiplication, and that worked because it left two independent blocks whose shapes were already known. Fixing the first multiplication instead would have been a disaster — after multiplying one adjacent pair, the chain changes shape and the remaining problem is no longer a clean sub-interval of the original. When you set up an interval table, always ask: what can I fix so that what remains falls into independent pieces of the same kind?

Others in this family use the same skeleton with a different combining rule: the longest palindromic subsequence, minimum insertions to make a palindrome, optimal binary search trees, and scoring games where two players take from the ends of a row. Whenever the input is a sequence but the solution is a tree of pairings, reach for an interval (i, j) state and try every split inside it.

Exponential Try Every Parenthesisation
O(N³) Time · O(N²) Space Interval Table