Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 14 – Dynamic programming

Interval DP: matrix chains and partitions

Solve a sequence problem on every interval with a split point: the matrix-chain order with the fewest multiplications, filled by length and read back.

  • Advanced
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 10 of 30 minutes free
Free: Why the order of multiplication matters · The interval recurrence and its fill order · Six matrices, filled and read back · 2 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 20 minutes.

What you will learn

  • Write DP over intervals [i, j] with a split point
  • Find the best order for a matrix-chain multiplication
  • Compute the table in order of interval length

Before you start

Interval dynamic programming solves a problem on a sequence by solving it on every contiguous stretch, every interval [i, j], from the shortest to the longest. The state is the interval, and the transition tries every split point k inside it, combining the best answers for [i, k] and [k + 1, j] with the cost of joining them. Matrix-chain multiplication is the standard example: A1 A2 … An is the same matrix whatever order the products are done in, but the number of scalar multiplications depends on that order, often by a large factor. With n matrices the table has about n²/2 cells, and each tries up to n splits, so the method takes O(n³) time and O(n²) memory. Cutting a string into palindromes, merging neighbouring piles and bursting balloons for points have the same shape.

Why the order of multiplication matters

Multiplying a p × q matrix by a q × r matrix takes p · q · r scalar multiplications and gives a p × r matrix. Matrix multiplication is associative, so (XW)V and X(WV) are equal, but their costs can be very different:

One product, two orders Python · order/two_orders.py
"""The same product of three matrices in two orders: the number of scalar multiplications differs fifty-fold."""


def cost(rows, inner, cols):
    """Multiplying a rows x inner matrix by an inner x cols matrix takes rows * inner * cols multiplications."""
    return rows * inner * cols


# X: 1,000 x 20 (a batch of rows), W: 20 x 500, V: 500 x 10
left_first = cost(1000, 20, 500) + cost(1000, 500, 10)  # (XW) is 1,000 x 500, then times V
right_first = cost(20, 500, 10) + cost(1000, 20, 10)  # (WV) is 20 x 10, then X times it
print(f"(XW)V: {left_first:,} multiplications")
print(f"X(WV): {right_first:,} multiplications")
print(f"the right order is {left_first // right_first} times cheaper, and the result is the same 1,000 x 10 matrix")

Output

(XW)V: 15,000,000 multiplications
X(WV): 300,000 multiplications
the right order is 50 times cheaper, and the result is the same 1,000 x 10 matrix

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_orders.py

The first order builds a 1,000 × 500 intermediate matrix and pays for it twice; the second keeps every intermediate small. Trying every order works only for short chains, because their number explodes. The number of orders for n matrices is the Catalan number C(n − 1), which grows roughly fourfold with every extra matrix.

The interval recurrence and its fill order

Store the dimensions in a list p of n + 1 numbers, so that matrix A_i is p[i − 1] × p[i]; this one-place offset is where most bugs come from. Let cost[i][j] be the fewest multiplications for the product A_i … A_j. A single matrix costs nothing: cost[i][i] = 0. Any order for a longer interval ends with one last product of two parts, A_i … A_k and A_k+1 … A_j, for some k between i and j − 1, and those parts should themselves be multiplied as cheaply as possible:

cost[i][j] = min over k of cost[i][k] + cost[k + 1][j] + p[i − 1] · p[k] · p[j]

Both parts are shorter than [i, j], so filling the table by interval length, 2 first, then 3 and so on up to n, finds every part ready. Recording the best k of each cell in a second table, split[i][j], lets you read the order back: put parentheses around the two parts of split[1][n] and repeat inside each part.

cost[i][j] adds the two shorter parts at every split k and the cost of multiplying their results, and keeps the smallest total.Two shorter intervals, already filledMultiply the two results:p[i − 1] × p[k] × p[j]cost[i][j] = the smallest totalover every split k = i … j − 1Fill by length 2, 3, …, n:every part of every split is shorter,so it is ready when neededcost[i][k]A_i … A_kthe left partcost[k + 1][j]A_k+1 … A_jthe right partorder

One cell of the interval table and the order that fills it

Text description of the diagram

The diagram runs from top to bottom.

  1. At the top, two boxes side by side are the two parts of a split at k: cost[i][k] for the matrices A_i to A_k, and cost[k + 1][j] for A_k+1 to A_j. Both intervals are shorter than i to j, so they are already filled.
  2. An arrow leads to the cost of multiplying the two results, a p[i − 1] × p[k] matrix by a p[k] × p[j] matrix: p[i − 1] × p[k] × p[j] scalar multiplications.
  3. The next box says that cost[i][j] is the smallest total over every split point k from i to j − 1.
  4. The last box gives the order: fill the table by interval length 2, 3 and so on up to n, so every part of every split is ready when a longer interval needs it.

Six matrices, filled and read back

The cheapest order for six matrices Python · chain/matrix_chain.py
"""The cheapest order to multiply six matrices: the interval table, filled by length, and the order read back."""

p = [12, 40, 8, 50, 6, 30, 16]  # matrix A_i is p[i - 1] x p[i]: A1 is 12 x 40, ..., A6 is 30 x 16
n = len(p) - 1


def matrix_chain(p):
    n = len(p) - 1
    cost = [[0] * (n + 1) for _ in range(n + 1)]  # cost[i][j]: cheapest way to multiply A_i ... A_j
    split = [[0] * (n + 1) for _ in range(n + 1)]
    for length in range(2, n + 1):  # shorter intervals first, so every part is ready
        for i in range(1, n - length + 2):
            j = i + length - 1
            cost[i][j] = min(cost[i][k] + cost[k + 1][j] + p[i - 1] * p[k] * p[j] for k in range(i, j))
            split[i][j] = next(k for k in range(i, j) if cost[i][k] + cost[k + 1][j] + p[i - 1] * p[k] * p[j] == cost[i][j])
    return cost, split


def order(split, i, j):
    if i == j:
        return f"A{i}"
    k = split[i][j]
    return f"({order(split, i, k)}{order(split, k + 1, j)})"


cost, split = matrix_chain(p)
print("cost[i][j] for i <= j:")
print("      " + "".join(f"{'A' + str(j):>8}" for j in range(1, n + 1)))
for i in range(1, n + 1):
    print(f"  A{i}: " + "".join(" " * 8 if j < i else f"{cost[i][j]:>8,}" for j in range(1, n + 1)))
left_to_right = sum(p[0] * p[k] * p[k + 1] for k in range(1, n))
print(f"best order {order(split, 1, n)}: {cost[1][n]:,} multiplications")
print(f"left to right (((((A1A2)A3)A4)A5)A6): {left_to_right:,} multiplications")

Output

cost[i][j] for i <= j:
            A1      A2      A3      A4      A5      A6
  A1:        0   3,840   8,640   6,816   8,976  10,848
  A2:                0  16,000   4,320  11,520  11,040
  A3:                        0   2,400   3,840   6,048
  A4:                                0   9,000   7,680
  A5:                                        0   2,880
  A6:                                                0
best order (((A1A2)(A3A4))(A5A6)): 10,848 multiplications
left to right (((((A1A2)A3)A4)A5)A6): 20,160 multiplications

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 matrix_chain.py

The answer is in the top-right cell: 10,848 multiplications with ((A1A2)(A3A4))(A5A6), against 20,160 for working left to right. Each row of the printed table is one starting matrix, and each cell was computed only after every shorter interval inside it. For six matrices the table checked 35 split points, (n³ − n)/6, where trying every order would mean 42 complete orders.

GATE CS

The GATE CS syllabus lists dynamic programming among its algorithm design techniques, and a short matrix chain is a compact way to practise it by hand. For matrices of 10 × 5, 5 × 20 and 20 × 4, (A1A2)A3 costs 1,000 + 800 = 1,800 and A1(A2A3) costs 400 + 200 = 600, so the answer is 600.

The same shape in other problems

The pattern is always the same: an interval, a choice inside it, and two smaller intervals that do not affect each other.

  • Palindrome partitioning: the fewest cuts that split a string into palindromes come from the same recurrence over substrings, where a piece that is already a palindrome needs no cut.
  • Merging neighbouring piles: when each merge costs the size of the new pile, an interval splits at its last merge.
  • Bursting balloons: the state chooses which balloon of an interval bursts last, so the two sides never touch.
  • Knuth’s optimisation: it narrows the split points of some of these problems and brings them down to O(n²) when its conditions hold.

Exercise and self-check

Return the fewest scalar multiplications for a chain and one order that achieves them. The tests rebuild the cost of your order from its parentheses.

Exercise · Medium · Python, JavaScript

The cheapest order to multiply a chain of matrices

Matrix A1 is dims[0] × dims[1], A2 is dims[1] × dims[2], and so on, so dims with n + 1 numbers describes a chain of n matrices. Multiplying a p × q matrix by a q × r matrix costs p · q · r scalar multiplications.

Write chain_order(dims) in chain.py (JavaScript: chainOrder(dims) in chain.mjs). It returns the fewest scalar multiplications needed to compute A1 A2 … An, and one order that achieves it, written with the names A1 to An and a pair of parentheses around every product: "A1" for a single matrix, "(A1A2)" for two, and for example "((A1A2)A3)" or "(A1(A2A3))" for three. Python returns a tuple (cost, order); JavaScript returns { cost, order }.

The sample tests check a single matrix, two worked chains and 200 random chains of up to 7 matrices, and they work out the cost of the order you return to make sure it matches.

Python · Starter code · chain.py

def chain_order(dims):
    """(cost, order): the fewest scalar multiplications for A1 ... An and one order, such as '((A1A2)A3)'."""
    # Replace this line: fill cost[i][j] by interval length, remember the best split, then build the order.
    return 0, ""
The sample tests · test_chain.py
import random
import re

from chain import chain_order


def evaluate(order, dims):
    """The cost of an order string, checking that it names A1 ... An once each, in order."""
    tokens = re.findall(r"\(|\)|A\d+", order)
    assert "".join(tokens) == order, f"unexpected characters in {order!r}"
    pos = 0
    nxt = [1]

    def expr():
        nonlocal pos
        assert pos < len(tokens), f"{order!r} ends too early"
        tok = tokens[pos]
        pos += 1
        if tok.startswith("A"):
            assert int(tok[1:]) == nxt[0], f"{order!r}: expected A{nxt[0]} at this point"
            i = nxt[0]
            nxt[0] += 1
            return dims[i - 1], dims[i], 0
        assert tok == "(", f"{order!r}: unexpected {tok}"
        r1, c1, k1 = expr()
        r2, c2, k2 = expr()
        assert pos < len(tokens) and tokens[pos] == ")", f"{order!r}: a product needs exactly two parts"
        pos += 1
        return r1, c2, k1 + k2 + r1 * c1 * c2

    rows, cols, cost = expr()
    assert pos == len(tokens) and nxt[0] == len(dims), f"{order!r} does not use every matrix exactly once"
    return cost


def slow_cost(dims, i, j):
    if i == j:
        return 0
    return min(slow_cost(dims, i, k) + slow_cost(dims, k + 1, j) + dims[i - 1] * dims[k] * dims[j] for k in range(i, j))


def check(dims, expected=None):
    cost, order = chain_order(list(dims))
    assert evaluate(order, dims) == cost, f"{order!r} does not cost {cost}"
    if expected is not None:
        assert cost == expected, (dims, cost)
    return cost


def test_small_chains():
    """a single matrix, two matrices and the three-matrix pipeline"""
    assert check([10, 20], 0) == 0
    assert check([3, 4, 5], 60) == 60
    assert check([1000, 20, 500, 10], 300_000) == 300_000


def test_six_matrices():
    """the six-matrix chain of the lesson"""
    assert check([12, 40, 8, 50, 6, 30, 16]) == 10_848


def test_random_chains():
    """on 200 random chains, as cheap as the best of every order"""
    rng = random.Random(9)
    for _ in range(200):
        dims = [rng.randint(1, 30) for _ in range(rng.randint(2, 8))]
        assert check(dims) == slow_cost(dims, 1, len(dims) - 1), dims

JavaScript · Starter code · chain.mjs

/** { cost, order }: the fewest scalar multiplications for A1 ... An and one order, such as '((A1A2)A3)'. */
export function chainOrder(dims) {
  // Replace this line: fill cost[i][j] by interval length, remember the best split, then build the order.
  return { cost: 0, order: '' };
}
The sample tests · chain.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { chainOrder } from './chain.mjs';

// The cost of an order string, checking that it names A1 ... An once each, in order.
function evaluate(order, dims) {
  const tokens = order.match(/\(|\)|A\d+/g) ?? [];
  assert.equal(tokens.join(''), order, `unexpected characters in "${order}"`);
  let pos = 0;
  let next = 1;
  const expr = () => {
    assert.ok(pos < tokens.length, `"${order}" ends too early`);
    const tok = tokens[pos++];
    if (tok.startsWith('A')) {
      assert.equal(Number(tok.slice(1)), next, `"${order}": expected A${next} at this point`);
      next++;
      return [dims[next - 2], dims[next - 1], 0];
    }
    assert.equal(tok, '(', `"${order}": unexpected ${tok}`);
    const [r1, c1, k1] = expr();
    const [, c2, k2] = expr();
    assert.ok(pos < tokens.length && tokens[pos] === ')', `"${order}": a product needs exactly two parts`);
    pos++;
    return [r1, c2, k1 + k2 + r1 * c1 * c2];
  };
  const [, , cost] = expr();
  assert.ok(pos === tokens.length && next === dims.length, `"${order}" does not use every matrix exactly once`);
  return cost;
}

function slowCost(dims, i, j) {
  if (i === j) return 0;
  let best = Infinity;
  for (let k = i; k < j; k++) best = Math.min(best, slowCost(dims, i, k) + slowCost(dims, k + 1, j) + dims[i - 1] * dims[k] * dims[j]);
  return best;
}

function check(dims) {
  const { cost, order } = chainOrder(dims.slice());
  assert.equal(evaluate(order, dims), cost, `"${order}" does not cost ${cost}`);
  return cost;
}

test('a single matrix, two matrices and the three-matrix pipeline', () => {
  assert.equal(check([10, 20]), 0);
  assert.equal(check([3, 4, 5]), 60);
  assert.equal(check([1000, 20, 500, 10]), 300000);
});

test('the six-matrix chain of the lesson', () => {
  assert.equal(check([12, 40, 8, 50, 6, 30, 16]), 10848);
});

test('on 200 random chains, as cheap as the best of every order', () => {
  let seed = 9;
  const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
  for (let t = 0; t < 200; t++) {
    const dims = Array.from({ length: 2 + Math.floor(random() * 7) }, () => 1 + Math.floor(random() * 30));
    assert.equal(check(dims), slowCost(dims, 1, dims.length - 1), JSON.stringify(dims));
  }
});
A hint

Let cost[i][j] be the cheapest way to multiply A_i … A_j. A single matrix costs 0. For a longer interval, try every split point k from i to j − 1: cost[i][k] + cost[k + 1][j] + dims[i − 1] · dims[k] · dims[j]. Fill the table by interval length, keep the best k of each cell in a second table, and build the string recursively from it.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Matrix Calculator Multiply two small matrices of different shapes and count the work by hand: a 2 × 3 matrix times a 3 × 4 matrix gives 2 × 4 entries, each a sum of 3 products, so 24 multiplications.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 A1 is 5 × 10, A2 is 10 × 3 and A3 is 3 × 12. What is the fewest number of scalar multiplications for A1 A2 A3?

    Type a number.

    Show the answer to question 1

    Answer: 330 multiplications

    (A1A2)A3 costs 5 · 10 · 3 = 150 for a 5 × 3 result, then 5 · 3 · 12 = 180: 330 in all. A1(A2A3) costs 10 · 3 · 12 = 360 for a 10 × 12 result, then 5 · 10 · 12 = 600: 960. The cheaper order keeps the small inner dimension, 3, in the first product.

  2. Question 2 of 4 Why is the interval table filled in order of interval length?

    Choose one answer.

    Show the answer to question 2

    Answer: Every split of an interval gives two shorter intervals, so their cells are ready when the longer one needs them

    cost[i][j] reads cost[i][k] and cost[k + 1][j] for every k between i and j − 1, and both intervals are shorter than [i, j]. Filling rows from the top instead would read cost[k + 1][j] from a lower row that is still empty.

  3. Question 3 of 4 How many split points k does the recurrence try for cost[2][6]?

    Type a number.

    Show the answer to question 3

    Answer: 4 split points

    k runs from i to j − 1, that is 2, 3, 4 and 5: four split points, one for each place where the product A2 … A6 can be divided into a left and a right part.

  4. Question 4 of 4 With the dimensions stored in a list p of n + 1 numbers, what are the dimensions of matrix A_i (counting from A1)?

    Choose one answer.

    Show the answer to question 4

    Answer: p[i − 1] × p[i]

    A1 is p[0] × p[1], A2 is p[1] × p[2], and so on, because neighbouring matrices share a dimension. Writing p[i] for the rows of A_i is the most common slip in matrix-chain code, and it gives wrong costs without any error.

Why the split recurrence is right, and the orders it never tries Pro: opens with a Pro, Premium or Ultimate pass

The recurrence is correct because an optimal order contains optimal orders for its two parts, and the table checks a cubic number of splits where the orders themselves are exponentially many. This part proves the first fact, counts the second against a brute force, and reproduces the two slips that break most matrix-chain code.

In this part:

  • Optimal parts make an optimal whole: the proof
  • The orders the table never tries, counted
  • Two slips that small tests miss

2 runnable examples (Python) · about 2 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Palindrome cuts, bursting balloons and merging piles, solved and measured Pro: opens with a Pro, Premium or Ultimate pass

Three classic partition problems use the interval shape, and each adds one idea: a cheaper 1-D form, a choice of the last event instead of the first, and a window that cuts the split points. This part solves each one, checks it against a brute force or a slower version, and counts the work.

In this part:

  • The fewest palindrome cuts, from O(n³) to O(n²)
  • Bursting balloons: choose the last balloon, not the first
  • Merging neighbouring piles, and Knuth’s window measured

3 runnable examples (Python) · about 2 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Matrix chains in four more languages, and a practice set Pro: opens with a Pro, Premium or Ultimate pass

The cubic loop looks the same in every language, and that makes it easy to check: one generated chain must cost the same in all of them. This part shows the chain in Python, JavaScript, Java, C++ and Go, compares the memoised and the table versions, and ends with a practice set.

In this part:

  • The chain in Python, JavaScript, Java, C++ and Go
  • Memoised or table, and how deep the recursion goes
  • Practice set

3 runnable examples (Build and run all three, C++, Go, Java, JavaScript, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Why does interval DP fill its table by increasing interval length instead of row by row from the top?

    Because the cell for an interval [i, j] reads the cells of the two parts of every split, [i, k] and [k + 1, j], and both parts are shorter than [i, j]. Filling by length, every interval of length 2 first, then 3 and so on, guarantees that every shorter interval is already done. Filling rows from the top does not: cost[1][3] needs cost[2][3], which lies in row 2 and is still empty, so the code silently reads 0 and returns a cost that is too low. For six matrices, filling by length means the 5 cells of length 2 first, then the 4 of length 3, and so on up to the single cell of length 6, which holds the answer.

  2. Mid to senior

    A teammate multiplies a chain of matrices by always doing the cheapest neighbouring product first. Show with a small chain that this can cost far more than the best order, and say what you would do instead.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    You must merge 400 sorted files that sit in a fixed order, and only neighbouring runs may be merged; merging two runs costs the sum of their sizes. How do you find the cheapest plan, how long does it take, and what changes if any two runs may be merged?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    Find the fewest cuts that split a string of 5,000 characters into palindromes within one second. Why is the plain interval DP too slow, and what do you compute instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Interval DP solves every interval [i, j] from the shortest up, trying every split point inside it.
  • Matrix chain: cost[i][j] = min over k of cost[i][k] + cost[k + 1][j] + p[i − 1] · p[k] · p[j], with cost[i][i] = 0.
  • Fill by interval length so that both parts of every split are ready; keep split[i][j] to read the order back.
  • The method takes O(n³) time and O(n²) memory, against a Catalan number of possible orders.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.