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.
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
What you’ll learn · Before you start
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
On this page
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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
One cell of the interval table and the order that fills it
Text description of the diagram
The diagram runs from top to bottom.
- 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.
- 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.
- The next box says that cost[i][j] is the smallest total over every split point k from i to j − 1.
- 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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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.
-
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?
-
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?
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
- On Efficient Computation of Matrix Chain Products (S. S. Godbole, IEEE Transactions on Computers C-22(9), 1973) (Institute of Electrical and Electronics Engineers (IEEE))
- Optimum binary search trees (D. E. Knuth, Acta Informatica 1, 1971) (Springer)
- Efficient dynamic programming using quadrangle inequalities (F. F. Yao, Proceedings of the 12th ACM Symposium on Theory of Computing, 1980) (Association for Computing Machinery (ACM))
- A000108: Catalan numbers (The OEIS Foundation)
- dynamic programming (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- GATE 2027 syllabus for Computer Science and Information Technology (CS) (IIT Madras (GATE 2027 organising institute))
- Computer Science Curricula 2023: Algorithmic Foundations (AL) (ACM, IEEE Computer Society and AAAI)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress