Master matrix chain multiplication as the gateway to interval DP — why multiplication order affects cost, the O(n³) interval DP solution, building the DP table from small intervals to large, reconstructing optimal parenthesization, and other interval DP problems like burst balloons and palindrome partitioning.
Why Order of Multiplication Matters
Matrix multiplication is associative: (A×B)×C = A×(B×C). The result is the same, but the number of scalar multiplications depends on the order.
Example: Three matrices: A(10×30), B(30×5), C(5×60)
| Option 1 | (A×B)×C |
| A×B: 10×30×5 = 1500 multiplications | result is 10×5 matrix |
| (A×B)×C | 10×5×60 = 3000 multiplications |
| Total | 4500 |
| Option 2 | A×(B×C) |
| B×C: 30×5×60 = 9000 multiplications | result is 30×60 matrix |
| A×(B×C) | 10×30×60 = 18000 multiplications |
| Total | 27000 |
Problem: Given n matrices, find the optimal parenthesization that minimizes scalar multiplications.
The Interval DP Solution
dp[i][j] = minimum cost to multiply matrices Aᵢ through Aⱼ.
Recurrence:
For all k from i to j-1
dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1] × p[k] × p[j])
Where
dp[i][k] = cost of left group: matrices i..k
dp[k+1][j] = cost of right group: matrices k+1..j
p[i-1]×p[k]×p[j] = cost of multiplying the two resulting matrices
Base case: dp[i][i] = 0 (single matrix, no multiplication needed)
Fill order: From shorter chains to longer chains (bottom-up by length).
Complete Implementation
def matrix_chain_multiplication(p):
"""
Find minimum cost to multiply matrix chain with dimensions p.
p[i-1] × p[i] = dimensions of matrix i.
Time: O(n³), Space: O(n²)
"""
n = len(p) - 1 # Number of matrices
# dp[i][j] = min cost to multiply matrices i..j (1-indexed)
dp = [[0]*n for _ in range(n)]
# Fill by chain length (from 2 to n)
for length in range(2, n+1): # length of chain
for i in range(n - length + 1): # starting index
j = i + length - 1 # ending index
dp[i][j] = float('inf')
# Try all split points k
for k in range(i, j):
# Cost of left × right + cost of combining
cost = dp[i][k] + dp[k+1][j] + p[i]*p[k+1]*p[j+1]
dp[i][j] = min(dp[i][j], cost)
return dp[0][n-1]
print(matrix_chain_multiplication([10, 30, 5, 60])) # 4500
print(matrix_chain_multiplication([40, 20, 30, 10, 30])) # 26000
DP Table Filling Visualization
For p = [10, 30, 5, 60] (3 matrices: A₁=10×30, A₂=30×5, A₃=5×60):
| Fill length=1 (single matrices) | dp[i][i] = 0 |
| dp[0][1] | Multiply A₁A₂ |
| k=0 | dp[0][0] + dp[1][1] + p[0]×p[1]×p[2] = 0+0+10×30×5 = 1500 |
| dp[1][2] | Multiply A₂A₃ |
| k=1 | dp[1][1] + dp[2][2] + p[1]×p[2]×p[3] = 0+0+30×5×60 = 9000 |
| dp[0][2] | Multiply A₁A₂A₃ |
| k=0 | (A₁)(A₂A₃) = dp[0][0]+dp[1][2]+p[0]×p[1]×p[3] = 0+9000+10×30×60 = 27000 |
| k=1 | (A₁A₂)(A₃) = dp[0][1]+dp[2][2]+p[0]×p[2]×p[3] = 1500+0+10×5×60 = 4500 |
Reconstruct Optimal Parenthesization
def mcm_with_split(p):
"""Find minimum cost AND the optimal split at each level."""
n = len(p) - 1
dp = [[0]*n for _ in range(n)]
split = [[0]*n for _ in range(n)] # split[i][j] = optimal k for range i..j
for length in range(2, n+1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
for k in range(i, j):
cost = dp[i][k] + dp[k+1][j] + p[i]*p[k+1]*p[j+1]
if cost < dp[i][j]:
dp[i][j] = cost
split[i][j] = k # Record optimal split point
def print_parens(split, i, j, names):
if i == j:
return names[i]
k = split[i][j]
left = print_parens(split, i, k, names)
right = print_parens(split, k+1, j, names)
return f"({left}×{right})"
names = [f"A{i+1}" for i in range(n)]
return dp[0][n-1], print_parens(split, 0, n-1, names)
cost, parenthesization = mcm_with_split([10, 30, 5, 60])
print(f"Minimum cost: {cost}") # 4500
print(f"Optimal: {parenthesization}") # (A1×(A2×A3)) or ((A1×A2)×A3)
Other Interval DP Problems
Burst Balloons
def max_coins_burst(nums):
"""
Burst all balloons, maximizing coins.
Coin from bursting balloon i = nums[i-1] × nums[i] × nums[i+1]
Key insight: think of it as the LAST balloon to burst in a range.
dp[i][j] = max coins from bursting all balloons in (i..j) exclusive.
"""
nums = [1] + nums + [1] # Add boundary balloons
n = len(nums)
dp = [[0]*n for _ in range(n)]
for length in range(2, n): # interval length
for left in range(n - length):
right = left + length
for k in range(left+1, right): # k = last balloon to burst
coins = nums[left]*nums[k]*nums[right]
dp[left][right] = max(dp[left][right],
dp[left][k] + dp[k][right] + coins)
return dp[0][n-1]
print(max_coins_burst([3, 1, 5, 8])) # 167
Palindrome Partitioning (Minimum Cuts)
def min_palindrome_cuts(s):
"""
Minimum cuts to partition s into all palindromes.
This is also interval DP.
"""
n = len(s)
# is_pal[i][j] = is s[i..j] a palindrome?
is_pal = [[False]*n for _ in range(n)]
for i in range(n): is_pal[i][i] = True
for i in range(n-1): is_pal[i][i+1] = (s[i]==s[i+1])
for length in range(3, n+1):
for i in range(n-length+1):
j = i+length-1
is_pal[i][j] = (s[i]==s[j]) and is_pal[i+1][j-1]
# dp[i] = min cuts for s[0..i]
dp = list(range(n)) # Worst case: cut before each character
for i in range(1, n):
if is_pal[0][i]:
dp[i] = 0
continue
for j in range(1, i+1):
if is_pal[j][i]:
dp[i] = min(dp[i], dp[j-1]+1)
return dp[n-1]
print(min_palindrome_cuts("aab")) # 1 ("a|ab" doesn't work; "aa|b" → 1)
print(min_palindrome_cuts("racecar")) # 0 (already a palindrome)
When to Use Interval DP
Interval DP fits problems where:
- You're deciding the last operation within a range [i, j]
- The solution for [i, j] depends on sub-ranges [i, k] and [k+1, j]
- You fill the DP table by increasing interval length
Pattern:
for length in range(2, n+1):
for i in range(n - length + 1):
j = i + length - 1
for k in range(i, j): # Try all split points
dp[i][j] = min/max(dp[i][j], f(dp[i][k], dp[k+1][j], ...))
Complexity
| Problem | Time | Space |
|---|
| Matrix Chain Multiplication | O(n³) | O(n²) |
| Burst Balloons | O(n³) | O(n²) |
| Palindrome Partitioning | O(n²) | O(n²) |
| Optimal BST | O(n³) | O(n²) |
All interval DP problems have O(n³) time due to the three nested loops: two for the range and one for the split point.
Summary
Matrix chain multiplication teaches interval DP — the technique for problems where you split a range at every possible point and combine results:
- State:
dp[i][j] = answer for subproblem on range [i, j] - Transition: try all split points k in [i, j-1]
- Fill order: by increasing interval length
- Time: O(n³) — three nested loops
This pattern appears in: matrix chaining, balloon bursting, palindrome partitioning, optimal BST construction, polygon triangulation, and more.
*Next Lesson: DP Interview Problems*