What is Matrix Chain Multiplication?

Matrix Chain Multiplication (MCM) asks: given a chain of matrices , in what order (parenthesization) should we multiply them to minimize the total number of scalar multiplications? Matrix multiplication is associative, so the result is the same regardless of order — but the cost varies dramatically. MCM is solved in time using interval Dynamic Programming.

Explanation

  • Multiplying a matrix by a matrix costs scalar multiplications.
  • For a chain of matrices, there are exponentially many parenthesizations (the Catalan number ), so brute force is infeasible for large .

The Key Insight

  • At some split point between and , we multiply (A_i ... A_k) with (A_{k+1} ... A_j).
  • The cost of this split is: cost(i,k) + cost(k+1,j) + p[i-1] * p[k] * p[j]
  • We try all valid values and take the minimum — this is the interval DP recurrence.

The DP Recurrence

  • Represent the chain by dimensions array p[] where matrix has dimensions p[i-1] × p[i].
  • dp[i][j] = minimum scalar multiplications to compute .
  • Base case: dp[i][i] = 0 (single matrix, no multiplication needed).
  • Recurrence:
dp[i][j] = min over k from i to j-1 of:
             dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j]
  • Fill by increasing chain length (length 2, then 3, …, then N).

Core Properties

  • Optimal Substructure: The optimal parenthesization of a chain contains optimal parenthesizations of its sub-chains.
  • Overlapping Subproblems: Many sub-chain costs are needed multiple times — DP avoids recomputation.
  • Interval DP Pattern: The key pattern is iterating over chain lengths from smallest to largest, not from left to right like linear DP.

How It Works

The Core Idea

  • Fill the DP table diagonally — start with chains of length 1 (cost 0), then length 2, then 3, up to length .
  • Each cell dp[i][j] tries all possible split points and takes the minimum total cost.
flowchart TD
    A["Input: dimensions array p[0..N]\nMatrix i has dims p[i-1] × p[i]"] --> B["Initialize dp[i][i] = 0 for all i"]
    B --> C["For chain_len = 2 to N"]
    C --> D["For i = 1 to N-chain_len+1\nj = i + chain_len - 1"]
    D --> E["dp[i][j] = ∞"]
    E --> F["For k = i to j-1\ncost = dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]"]
    F --> G{"cost < dp[i][j]?"}
    G -- Yes --> H["dp[i][j] = cost\nsplit[i][j] = k"]
    G -- No --> I["Try next k"]
    H --> I
    I --> F
    F --> J["dp[1][N] = minimum total cost"]

    classDef default fill:#1f2937,stroke:#3b82f6,stroke-width:2px,color:#fff;

Step-by-Step Trace (3 Matrices: A₁=10×30, A₂=30×5, A₃=5×60)

Dimensions array: p = [10, 30, 5, 60]
Matrices: A1(10×30), A2(30×5), A3(5×60)

Initialize: dp[1][1]=0, dp[2][2]=0, dp[3][3]=0

Chain length = 2:
  i=1, j=2: k=1
    cost = dp[1][1] + dp[2][2] + p[0]*p[1]*p[2]
         = 0 + 0 + 10*30*5 = 1500
    dp[1][2] = 1500, split[1][2] = 1

  i=2, j=3: k=2
    cost = dp[2][2] + dp[3][3] + p[1]*p[2]*p[3]
         = 0 + 0 + 30*5*60 = 9000
    dp[2][3] = 9000, split[2][3] = 2

Chain length = 3:
  i=1, j=3:
    k=1: cost = dp[1][1] + dp[2][3] + p[0]*p[1]*p[3]
               = 0 + 9000 + 10*30*60 = 27000
    k=2: cost = dp[1][2] + dp[3][3] + p[0]*p[2]*p[3]
               = 1500 + 0 + 10*5*60 = 4500 ← minimum!
    dp[1][3] = 4500, split[1][3] = 2

Result: Minimum multiplications = dp[1][3] = 4500
Optimal parenthesization: split at k=2 → (A1 × A2) × A3

Verification:
  Option 1: (A1×A2)×A3 = 1500 + 9000 = ... wait
  Actually:
  k=2 means: (A1A2) × A3
    Cost(A1×A2) = 10×30×5 = 1500
    Cost((A1A2)×A3) = 10×5×60 = 3000
    Total = 1500 + 3000 = 4500 ✅

  k=1 means: A1 × (A2A3)
    Cost(A2×A3) = 30×5×60 = 9000
    Cost(A1×(A2A3)) = 10×30×60 = 18000
    Total = 9000 + 18000 = 27000 ❌ (much worse)

Complexity Analysis

ScenarioTime ComplexitySpace ComplexityNotes
Naive RecursionO(2^N)O(N) stackCatalan number of parenthesizations
Memoization (Top-Down)O(N³)O(N²) memo tableRecursive with cache
Tabulation (Bottom-Up)O(N³)O(N²) dp tableIterative, cache-friendly

Why O(N³)?

  • There are sub-problems (all pairs [i,j]).
  • Each sub-problem tries up to split points .
  • Total: .

Implementation

  • Matrix Chain Multiplication — Bottom-Up DP with Optimal Parenthesization split[][] table.

    The implementation below computes the minimum cost and reconstructs the optimal parenthesization string via the

def matrix_chain_order(p: list[int]) -> tuple[int, str]:
    """
    p[i] = rows of matrix i, p[i+1] = cols of matrix i (1-indexed matrices).
    Returns (min_cost, optimal_parenthesization).
    """
    n = len(p) - 1  # Number of matrices
    # dp[i][j] = min cost to multiply matrices i..j (1-indexed)
    dp = [[0] * (n + 1) for _ in range(n + 1)]
    split = [[0] * (n + 1) for _ in range(n + 1)]
 
    # Fill by increasing chain length
    for length in range(2, n + 1):          # chain length
        for i in range(1, n - length + 2):  # start index
            j = i + length - 1              # end index
            dp[i][j] = float('inf')
            for k in range(i, j):            # split point
                cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j]
                if cost < dp[i][j]:
                    dp[i][j] = cost
                    split[i][j] = k
 
    def reconstruct(i, j) -> str:
        if i == j:
            return f"A{i}"
        k = split[i][j]
        return f"({reconstruct(i, k)} × {reconstruct(k+1, j)})"
 
    return dp[1][n], reconstruct(1, n)
 
# Example: A1(10×30), A2(30×5), A3(5×60)
p = [10, 30, 5, 60]
cost, parens = matrix_chain_order(p)
print(f"Min Cost: {cost}")        # 4500
print(f"Optimal: {parens}")       # ((A1 × A2) × A3)
#include <iostream>
#include <vector>
#include <climits>
#include <string>
 
std::pair<int, std::string> matrixChainOrder(const std::vector<int>& p) {
    int n = p.size() - 1;
    std::vector<std::vector<int>> dp(n + 1, std::vector<int>(n + 1, 0));
    std::vector<std::vector<int>> split(n + 1, std::vector<int>(n + 1, 0));
 
    for (int len = 2; len <= n; ++len) {
        for (int i = 1; i <= n - len + 1; ++i) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; ++k) {
                int cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j];
                if (cost < dp[i][j]) {
                    dp[i][j] = cost;
                    split[i][j] = k;
                }
            }
        }
    }
 
    // Reconstruct parenthesization
    std::function<std::string(int, int)> reconstruct = [&](int i, int j) -> std::string {
        if (i == j) return "A" + std::to_string(i);
        return "(" + reconstruct(i, split[i][j]) + " × " + reconstruct(split[i][j]+1, j) + ")";
    };
 
    return {dp[1][n], reconstruct(1, n)};
}
 
int main() {
    std::vector<int> p = {10, 30, 5, 60};
    auto [cost, parens] = matrixChainOrder(p);
    std::cout << "Min Cost: " << cost << "\n";   // 4500
    std::cout << "Optimal: " << parens << "\n";  // ((A1 × A2) × A3)
    return 0;
}
function matrixChainOrder(p) {
    const n = p.length - 1;
    const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0));
    const split = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0));
 
    for (let len = 2; len <= n; len++) {
        for (let i = 1; i <= n - len + 1; i++) {
            const j = i + len - 1;
            dp[i][j] = Infinity;
            for (let k = i; k < j; k++) {
                const cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j];
                if (cost < dp[i][j]) {
                    dp[i][j] = cost;
                    split[i][j] = k;
                }
            }
        }
    }
 
    function reconstruct(i, j) {
        if (i === j) return `A${i}`;
        return `(${reconstruct(i, split[i][j])} × ${reconstruct(split[i][j] + 1, j)})`;
    }
 
    return { cost: dp[1][n], parens: reconstruct(1, n) };
}
 
const p = [10, 30, 5, 60];
const { cost, parens } = matrixChainOrder(p);
console.log("Min Cost:", cost);    // 4500
console.log("Optimal:", parens);  // ((A1 × A2) × A3)
public class MatrixChainMultiplication {
    static int[][] dp, split;
 
    public static int matrixChainOrder(int[] p) {
        int n = p.length - 1;
        dp = new int[n + 1][n + 1];
        split = new int[n + 1][n + 1];
 
        for (int len = 2; len <= n; len++) {
            for (int i = 1; i <= n - len + 1; i++) {
                int j = i + len - 1;
                dp[i][j] = Integer.MAX_VALUE;
                for (int k = i; k < j; k++) {
                    int cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j];
                    if (cost < dp[i][j]) {
                        dp[i][j] = cost;
                        split[i][j] = k;
                    }
                }
            }
        }
        return dp[1][n];
    }
 
    static String reconstruct(int i, int j) {
        if (i == j) return "A" + i;
        return "(" + reconstruct(i, split[i][j]) + " × " + reconstruct(split[i][j] + 1, j) + ")";
    }
 
    public static void main(String[] args) {
        int[] p = {10, 30, 5, 60};
        int cost = matrixChainOrder(p);
        System.out.println("Min Cost: " + cost);                // 4500
        System.out.println("Optimal: " + reconstruct(1, 3));   // ((A1 × A2) × A3)
    }
}

Alternative Variant (Top-Down Memoization)

  • Top-Down Memoization — Easier to Write, Same Complexity memo[i][j] and return cached results. Same time and space as bottom-up tabulation.

    The recursive memoized version is often easier to reason about: define

from functools import lru_cache
 
def matrix_chain_memo(p: list[int]) -> int:
    n = len(p) - 1
 
    @lru_cache(maxsize=None)
    def solve(i: int, j: int) -> int:
        if i == j:
            return 0
        return min(
            solve(i, k) + solve(k + 1, j) + p[i - 1] * p[k] * p[j]
            for k in range(i, j)
        )
 
    return solve(1, n)
 
# Example
p = [10, 30, 5, 60]
print(matrix_chain_memo(p))  # 4500
function matrixChainMemo(p) {
    const n = p.length - 1;
    const memo = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(-1));
 
    function solve(i, j) {
        if (i === j) return 0;
        if (memo[i][j] !== -1) return memo[i][j];
        let best = Infinity;
        for (let k = i; k < j; k++) {
            const cost = solve(i, k) + solve(k + 1, j) + p[i-1] * p[k] * p[j];
            if (cost < best) best = cost;
        }
        return memo[i][j] = best;
    }
 
    return solve(1, n);
}
 
console.log(matrixChainMemo([10, 30, 5, 60])); // 4500

When to Use Matrix Chain Multiplication

flowchart TD
    Q{"Do you have a chain of\noperations where order\naffects total cost?"}
    Q -- No --> R1["Use simpler greedy\nor linear DP"]
    Q -- Yes --> S1{"Is the operation\nassociative?"}
    S1 -- No --> R2["Order is fixed;\nMCM doesn't apply"]
    S1 -- Yes --> S2{"Is the cost function\ndecomposable into\nsubproblems?"}
    S2 -- No --> R3["Use brute-force\nor heuristics"]
    S2 -- Yes --> R4["✅ Use Interval DP\n(MCM pattern, O(N³))"]

    classDef default fill:#1f2937,stroke:#3b82f6,stroke-width:2px,color:#fff;

✅ Use MCM / Interval DP When

  • You need to find the optimal parenthesization of a chain of matrix multiplications.
  • Any problem that requires optimal way to split a sequence into sub-sequences (Burst Balloons, Palindrome Partitioning, Optimal BST).
  • Compiler optimization — reordering matrix operations in linear algebra kernels (BLAS, ML frameworks).

❌ Avoid MCM When

  • The number of matrices is very large () — may be too slow; consider approximation algorithms.
  • The operation is not associative — order matters for correctness, not just cost.
  • You only need to compute the product once with a fixed order — just multiply sequentially.

Key Takeaways

  • Order ≠ Result, But Order = Cost — Matrix multiplication is associative (same result), but parenthesization dramatically affects the number of scalar operations.
  • Interval DP Pattern — Fill the DP table by increasing chain lengths (2, 3, …, N), not row by row. This is the defining characteristic of interval DP.
  • O(N³) Optimal — Three nested loops: chain length, start index, split point. Each sub-problem solved once → total.
  • Split Table for Reconstruction — Store the optimal split point in a split[][] table and recursively reconstruct the parenthesization string.
  • Generalizable Pattern — MCM is the canonical interval DP problem. The same pattern applies to Burst Balloons, Palindrome Partitioning II, Optimal BST, and Stone Merging.
  • Exponential Brute Force — There are Catalan number parenthesizations; for that’s 4862. DP reduces this to a polynomial .

More Learn

GitHub & Webs