Matrix Chain Multiplication
Choose the parenthesization of a matrix product that minimizes scalar multiplications — the archetypal interval DP.
Overview
Multiplying matrices A₁ × A₂ × … × Aₙ gives the same result regardless of parenthesization (associativity), but the cost differs enormously. Multiplying a p×q by a q×r matrix costs p·q·r scalar multiplications. Given dimensions d[0..n] where Aᵢ is d[i-1] × d[i], find the parenthesization with minimum total cost.
This is the canonical interval DP (see Interval (Range) DP): the state is a contiguous range of matrices, the transition tries every split point inside it, and ranges are solved in increasing length. Burst Balloons, optimal BST, and polygon triangulation share the exact structure.
Intuition
A mental model before the formal terms.
Three matrices with dimensions 10×30, 30×5, 5×60. (A₁A₂)A₃ costs 10·30·5 + 10·5·60 = 1500 + 3000 = 4500. A₁(A₂A₃) costs 30·5·60 + 10·30·60 = 9000 + 18000 = 27000. Same result, six times the work. The best order shrinks the "middle" dimension early.
For a chain, whatever you do, there is one *last* multiplication that joins a left group and a right group. If you knew where that split was, each side would be an independent, smaller chain problem. You do not know it, so try every split and keep the cheapest — but store each range's answer so the same sub-chain is never solved twice.
How it works
- State:
dp[i][j]= minimum cost to multiply matricesAᵢ … Aⱼ(1-indexed), whose product isd[i-1] × d[j]. - Transition:
dp[i][j] = min over k in [i, j-1] of dp[i][k] + dp[k+1][j] + d[i-1]·d[k]·d[j]— cost of the left group, the right group, and the final multiplication of ad[i-1]×d[k]by ad[k]×d[j]. - Base case:
dp[i][i] = 0— a single matrix needs no multiplication. - Iteration order: by increasing interval length
len = 2..n; for eachi,j = i + len - 1. Everydp[i][k]anddp[k+1][j]is a strictly shorter interval, so it is already filled. Top-down recursion on(i, j)with memoization avoids thinking about the order. - Answer location:
dp[1][n]. Store the bestkinsplit[i][j]to print the parenthesization recursively. - Space optimization: none of the usual rolling tricks apply —
dp[i][j]needs arbitrary shorter intervals, not just an adjacent row. The table isO(n²); only the upper triangle is used.
Why it works
Optimal substructure: in an optimal parenthesization of Aᵢ…Aⱼ, the outermost multiplication splits the chain at some k. The parenthesization of Aᵢ…Aₖ inside it must itself be optimal — if a cheaper one existed, substituting it would lower the total without affecting the right side or the final multiplication cost (which depends only on d[i-1], d[k], d[j]). Same for the right side. Minimizing over all k therefore finds the optimum.
Overlapping subproblems: the number of parenthesizations is the Catalan number C(n-1) (exponential), but there are only n(n+1)/2 intervals. Each interval does O(n) work over splits, giving O(n³).
Processing by increasing length is a valid topological order of the dependency DAG because every dependency is a proper sub-interval.
Recognition
How to tell a problem wants this.
- A sequence where the cost of combining a range depends on a split point inside it and the range boundaries.
- "Minimum cost to parenthesize / merge / cut / remove" over a contiguous sequence;
n ≤ 500(cubic is fine). - Boundary elements stay fixed while the interior is resolved — hallmark of interval DP.
Interactive visualization
Play, step, change the input. ← → and space work too.
| A1 | A2 | A3 | |
|---|---|---|---|
| A1 | 0 | · | · |
| A2 | · | 0 | · |
| A3 | · | · | 0 |
1dp[i][i] = 02for len in 2 .. n:3 for i in 1 .. n-len+1: j = i+len-14 dp[i][j] = ∞5 for k in i .. j-1:6 cost = dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]7 if cost < dp[i][j]: dp[i][j] = cost; split[i][j] = k8return dp[1][n]Pseudocode
1dp[i][i] = 0 for all i2for len in 2..n:3 for i in 1..n-len+1:4 j = i + len - 1; dp[i][j] = INF5 for k in i..j-1:6 dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + d[i-1]*d[k]*d[j])7return dp[1][n]Implementations
1import math2 3# Matrix chain multiplication: choose the parenthesisation that minimises4# scalar multiplications. dims has n+1 entries: matrix i is dims[i] x dims[i+1].5# This is the archetypal interval DP — solve short ranges first, and every6# longer range is a split into two already-solved halves.7 8 91 · dp[i][j] = min cost to multiply matrices i..j; split[i][j] records where10def matrix_chain_order(dims: list[int]) -> tuple[int, list[list[int]]]:11 n = len(dims) - 1 # number of matrices12 if n <= 0:13 return 0, []14 dp = [[0] * n for _ in range(n)]15 split = [[-1] * n for _ in range(n)]16 172 · Grow by chain length: length 1 costs 0, so start at 218 for length in range(2, n + 1):19 for i in range(n - length + 1):20 j = i + length - 121 best = math.inf22 best_k = -1233 · Try every split point; both halves are already final24 for k in range(i, j):25 cost = dp[i][k] + dp[k + 1][j] + dims[i] * dims[k + 1] * dims[j + 1]26 if cost < best:27 best, best_k = cost, k28 dp[i][j] = int(best)29 split[i][j] = best_k30 return dp[0][n - 1], split31 32 334 · The split table reconstructs the parenthesisation34def build_parens(split: list[list[int]], i: int, j: int) -> str:35 if i == j:36 return f"A{i}"37 k = split[i][j]38 return f"({build_parens(split, i, k)}{build_parens(split, k + 1, j)})"39 40 415 · Why order matters: the same product can cost wildly different amounts42def cost_of_left_to_right(dims: list[int]) -> int:43 n = len(dims) - 144 total = 045 rows = dims[0]46 for i in range(1, n):47 total += rows * dims[i] * dims[i + 1]48 # rows stays dims[0] because the accumulated product keeps its row count49 return total[[0] * n for _ in range(n)]builds n distinct rows;[[0] * n] * nwould alias one row n times — the single most common Python 2D-array bug.best = math.infstarts the inner minimisation; the finalint(best)converts back, sincemath.infis a float and the costs are integers.- Accumulating into a local
bestand only then writingdp[i][j]avoids repeatedly indexing the table inside the innermost loop. - Python integers are unbounded, so the triple product
dims[i] * dims[k+1] * dims[j+1]is always exact regardless of matrix size. f"A{i}"and the nested f-string inbuild_parensproduce the bracketing without concatenation.
The triple loop is pure Python, so this is one of the slower entries in the group — functools.lru_cache on a recursive form is often more readable at the same cost.
[[0] * n] * naliases; the list comprehension is the fix, and this bug appears in every 2D DP written in Python.math.infis a float, so mixing it into an integer table needs theint()conversion — using a large integer sentinel avoids that entirely.functools.lru_cacheon a recursivecost(i, j)expresses interval DP very naturally, at the cost of O(n^2) cache entries and recursion depth O(n).numpydoes not help here: the recurrence is inherently sequential over increasing lengths.
- Using
[[0] * n] * nand having every row alias the same list. - Leaving
math.infin an integer table, sodp[i][j]is a float and comparisons downstream become float comparisons. - Writing the recursive form without
lru_cache, which is exponential.
- Building a 2D table safely is the shared trap, and it has the same shape in two languages:
[[0] * n] * nin Python andnew Array(n).fill(new Array(n))in JS/TS both alias one row; C++std::vector<std::vector<T>>(n, std::vector<T>(n))value-initialises n distinct rows. - The infinity sentinel: JS/TS
Infinityand Pythonmath.infcompare and saturate cleanly, while C++ needsLLONG_MAXand the discipline never to add to it. - Overflow on the triple product: real in C++ without
long long, silent past 2^53 in JS/TS, and impossible in Python. - String building for the reconstruction: C++ concatenation (or
std::format), JS/TS template literals, and Python f-strings — the latter two nest cleanly, which matters for a recursive bracketing.
Complexity
Hu–Shing solves MCM specifically in O(n log n), but the cubic interval DP is the general template.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Optimal order of associative binary combinations over a sequence where combination cost depends on the operands' "shape".
- Any interval DP with a split point: Burst Balloons, Minimum Cost to Cut a Stick, Optimal BST, polygon triangulation, boolean parenthesization.
nup to a few hundred.
- The combination order does not affect cost (e.g. summing numbers) — nothing to optimize.
n ≥ 5000— cubic is too slow; look for Knuth optimization (quadrangle inequality) or a problem-specificO(n log n)method.- The "split" is not contiguous (subset-based rather than interval-based) — that is Bitmask DP territory.
Alternatives
Common mistakes
- Iterating
iandjin plain row-major order —dp[k+1][j]fork+1 > iis not yet computed. Iterate by interval length (or use memoized recursion). - Off-by-one in the dimension array: matrix
iisd[i-1] × d[i], son = len(d) - 1. - Using
intfor costs:500³intermediate products overflow 32-bit. - In Burst Balloons, choosing the *first* balloon to burst instead of the *last* — the interval boundaries must remain fixed while the interior is solved.
Interview patterns
- Matrix Chain Order: minimum multiplication cost and the parenthesization.
- Burst Balloons:
dp[i][j] = max over k of dp[i][k] + dp[k][j] + nums[i]·nums[k]·nums[j]with sentinel 1s. - Minimum Cost to Cut a Stick / Minimum Score Triangulation of Polygon.
- Palindrome Partitioning II and "remove boxes" — interval DP with extra state.
- Coin ChangeIntermediate