Interval DP (DP over Ranges [i..j]) — Junior Level¶
One-line summary: Interval DP solves a problem on a sequence by computing the best answer for every contiguous range
[i..j], building from the shortest ranges up to the whole sequence. The defining recurrence isdp[i][j] = best over a split point k of dp[i][k] + dp[k+1][j] + cost(i, k, j), and you fill the table in order of increasing interval length.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Step-by-Step Walkthrough
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
Imagine someone hands you a row of numbers and asks: "In what order should you combine adjacent items to pay the least total cost?" A classic version is matrix chain multiplication: you have a chain of matrices M₁ · M₂ · ⋯ · Mₙ, and although the final product is the same no matter how you parenthesize, the number of scalar multiplications depends drastically on the order. (M₁ M₂) M₃ may cost a thousand operations while M₁ (M₂ M₃) costs a million. Which parenthesization is cheapest?
There are exponentially many ways to parenthesize, so brute force is hopeless for even a few dozen matrices. The breakthrough is a simple observation: whatever the optimal parenthesization is, there is some outermost multiplication that splits the chain into a left part and a right part. If we knew the best way to handle each part, we could try every possible split point and take the cheapest combination. That is the heart of interval DP.
The technique works on any problem where:
- the input is a linear sequence (numbers, characters, matrices, stones, balloons), and
- the answer for a range
[i..j]is built from the answers of smaller sub-ranges inside it.
We define dp[i][j] = the best (minimum cost, maximum value, count, etc.) answer for the contiguous sub-sequence from index i to index j. The general recurrence picks a split point k inside the range:
dp[i][j] = best over k of ( dp[i][k] + dp[k+1][j] + cost(i, k, j) )
We try every k between i and j, combine the two sub-answers plus the cost of joining them, and keep the best. To make sure the sub-ranges are already solved before we need them, we fill the table by increasing interval length: first all length-1 ranges (the base cases), then length-2, then length-3, and so on, up to the full range [0..n-1].
Two running examples appear throughout this file:
- Matrix chain multiplication — split-point flavor: choose the outermost
×. - Burst balloons — last-operated-element flavor: choose which balloon is burst last inside the range.
Both are interval DP; they differ only in what the split point means. Mastering that one distinction unlocks the whole family: optimal binary search trees, stone merging, palindrome partitioning, and removing boxes, all covered in this topic's middle.md and beyond.
The baseline cost is O(n³): there are O(n²) intervals, and each tries O(n) split points. For some problems that cubic factor can be cut to O(n²) using Knuth's optimization or the divide-and-conquer optimization — pointers to sibling topics 11 and 12 — but the O(n³) interval DP is the foundation you must understand first.
Prerequisites¶
Before reading this file you should be comfortable with:
- Basic dynamic programming — the idea of storing sub-answers in a table and reusing them (overlapping subproblems, optimal substructure). See sibling
01-introduction-to-dpand02-memoization-vs-tabulation. - 2D arrays / nested loops — every interval DP lives in a 2D table
dp[i][j]. - Recursion and memoization — the top-down version of interval DP is recursion + a cache.
- Big-O notation —
O(n²)intervals,O(n)splits,O(n³)total. - Prefix sums — many interval problems (stone merging) precompute range sums for
O(1)cost lookups.
Optional but helpful:
- A glance at matrix multiplication dimensions — to follow the matrix chain example (an
a × bmatrix times ab × cmatrix costsa·b·cscalar multiplications and yields ana × cmatrix). - Familiarity with binary search trees — to follow the optimal BST example in
middle.md.
Glossary¶
| Term | Meaning |
|---|---|
Interval / range [i..j] | A contiguous sub-sequence from index i to index j, inclusive. |
dp[i][j] | The optimal answer (min cost / max value / count) for the sub-sequence [i..j]. |
Split point k | An index inside [i..j] where the range is divided into two parts to combine. |
| Last-operated element | An alternative framing: the element inside [i..j] that is processed last (e.g., the balloon burst last). |
| Interval length | j - i + 1, the number of elements in the range. The fill order is by increasing length. |
| Base case | The answer for the smallest ranges, usually length-1 (a single element) where no work is needed (dp[i][i] = 0 or similar). |
cost(i, k, j) | The extra cost of combining the two sub-ranges at split k (e.g., the scalar multiplications for one matrix product). |
| Optimal substructure | The property that an optimal answer for [i..j] is built from optimal answers of its sub-ranges. |
| Tabulation | Bottom-up filling of the dp table; the natural style for interval DP (by increasing length). |
| Memoization | Top-down recursion that caches results for each (i, j) pair. |
Core Concepts¶
1. The State Is an Interval¶
The single most important idea: in interval DP the state is a contiguous range [i..j], and dp[i][j] holds the best answer for just that range. Because i and j are both indices into a length-n sequence with i ≤ j, there are about n²/2 distinct states. We store them in a 2D table.
For matrix chain multiplication on matrices M_i, …, M_j, dp[i][j] is the minimum number of scalar multiplications to compute that sub-product. For burst balloons, dp[i][j] is the maximum coins obtainable by bursting all balloons strictly inside the open range (i, j).
2. The Transition Chooses a Split (or a Last Element)¶
There are two equivalent ways to set up the recurrence, and recognizing which fits your problem is the key skill.
Split-point framing (matrix chain). The optimal answer has some outermost operation that divides [i..j] into [i..k] and [k+1..j]:
You first compute both halves optimally, then pay the cost of joining them. For matrix chain, cost(i, k, j) is the cost of multiplying the result of the left part (an r_i × r_{k+1} matrix) by the result of the right part (an r_{k+1} × r_{j+1} matrix), which is r_i · r_{k+1} · r_{j+1} scalar multiplications.
Last-element framing (burst balloons). Sometimes it is cleaner to ask: which element of [i..j] is operated on last? If element k is burst last (after everything else in the range is gone), then when you burst it its neighbors are exactly the range boundaries:
The two halves dp[i][k] and dp[k][j] are solved first (everything except k), and value(i, k, j) accounts for the last operation on k. This "last element" trick fixes the tricky issue that bursting changes neighbors — by choosing the last one, its neighbors are stable.
3. Fill by Increasing Length¶
Both recurrences read sub-ranges that are strictly smaller than [i..j]. So when we fill dp[i][j], every smaller range it depends on must already be computed. The clean way to guarantee this is to iterate by interval length:
for length = 1 to n: # or start at 2, depending on the problem
for i = 0 to n - length:
j = i + length - 1
dp[i][j] = ... (uses dp of smaller ranges)
Length-1 ranges are the base cases. Then length-2, length-3, …, finishing with the single length-n range [0..n-1], whose value is the final answer. This length-first ordering is the signature of interval DP — if you see it, you are almost certainly looking at interval DP.
4. Base Cases¶
The base case is the smallest range that needs no combining. For matrix chain, a single matrix needs zero multiplications: dp[i][i] = 0. For burst balloons with the open-range framing, an empty interior contributes nothing. Getting the base case right (and starting the length loop at the right value) is where most beginner bugs live.
5. The cost / value Function¶
The split recurrence is generic; what makes each problem specific is the cost(i, k, j) term. Examples you will meet:
- Matrix chain:
cost = r_i · r_{k+1} · r_{j+1}(dimensions of the two factors). - Stone merging:
cost = sum(stones[i..j])(the merge cost is the total weight, independent ofk). - Optimal BST:
cost = sum of access frequencies in [i..j](every key gets one deeper level). - Burst balloons:
value = nums[i] · nums[k] · nums[j](the coins from burstingklast between boundariesiandj).
6. Reconstructing the Answer (optional)¶
dp[0][n-1] gives the optimal value. If you also need the choice (which parenthesization, which BST shape), store the winning split k in a second table split[i][j] whenever you update dp[i][j], then walk that table recursively afterward.
Big-O Summary¶
| Quantity | Value | Notes |
|---|---|---|
| Number of states (intervals) | O(n²) | All pairs i ≤ j. |
| Work per state (try every split) | O(n) | Loop k from i to j. |
| Total time (baseline) | O(n³) | n² states × n splits. |
| Space | O(n²) | The dp table (plus optional split table). |
cost(i, k, j) lookup | O(1) | With prefix sums where needed. |
| With Knuth's optimization | O(n²) | Only when the quadrangle inequality holds (see 11). |
| With divide-and-conquer optimization | O(n² log n) | Different monotonicity condition (see 12). |
The headline baseline number is O(n³) — cubic in the sequence length. That is fine for n up to a few hundred (around n ≤ 500 is comfortable). Beyond that, you look at the optimizations.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
Interval [i..j] | A consecutive stretch of a railway line between station i and station j; you reason about each stretch independently. |
Split point k | Deciding where to place a junction that joins a left segment and a right segment of track. |
dp[i][j] | The cheapest way to build the whole stretch from i to j. |
| Cost of joining | The extra construction cost at the junction itself. |
| Fill by increasing length | Build all the short segments first, then medium, then the full line — you can't price a long segment until its pieces are priced. |
| Last-operated element (balloons) | Removing items from a row of dominoes: it's easiest to reason about which one you knock down last, because by then its neighbors are fixed. |
| Matrix chain | Choosing the order to merge a stack of reports: merging two small reports first is cheaper than merging a giant one with a tiny one repeatedly. |
Where the analogy breaks: real construction costs are additive in obvious ways; interval DP's power is that the combination cost can depend on the whole range, and trying every split is what discovers the non-obvious cheapest plan.
Pros & Cons¶
| Pros | Cons |
|---|---|
Turns an exponential "try every parenthesization" search into polynomial O(n³). | Cubic time limits it to sequences of a few hundred elements without optimization. |
| One uniform template (state = interval, transition = split) covers a whole family of classic problems. | Requires the problem to decompose along a contiguous range; doesn't apply to arbitrary subsets. |
| The "last-operated element" trick elegantly handles problems where operations change neighbors (balloons, removing boxes). | Choosing split-point vs last-element framing is subtle; the wrong framing can make the DP incorrect. |
O(n²) space is modest; easy to reconstruct the optimal choice with a parallel table. | Some variants (removing boxes) need a third state dimension, raising cost to O(n⁴). |
| Sets up cleanly for Knuth / D&C optimizations when the cost function is well-behaved. | Off-by-one errors in the length loop and base cases are very common. |
When to use: the input is a linear sequence; the optimal answer for a range is built from optimal answers of contiguous sub-ranges; you must choose an order/grouping/split (parenthesization, merging, partitioning, last removal). Classic: matrix chain, optimal BST, burst balloons, stone merging, palindrome partitioning, removing boxes.
When NOT to use: the subproblems are over arbitrary subsets (use bitmask DP); the sequence is huge (n in the thousands+) and no optimization applies; the problem has no contiguous-range structure (e.g., it's a graph or tree problem in disguise — use a different DP).
Step-by-Step Walkthrough¶
Let's do matrix chain multiplication by hand on a tiny chain. We have 4 matrices with dimensions given by dims = [10, 30, 5, 60], meaning:
M₀is10 × 30M₁is30 × 5M₂is5 × 60
So matrix M_i is dims[i] × dims[i+1]. We want the minimum scalar multiplications to compute M₀ · M₁ · M₂. Here n = 3 matrices, indexed 0..2.
Base case (length 1). A single matrix needs no multiplication:
Length 2. Two ranges: [0..1] and [1..2].
dp[0][1]: only split k = 0
= dp[0][0] + dp[1][1] + dims[0]*dims[1]*dims[2]
= 0 + 0 + 10*30*5 = 1500 // (M₀ M₁): a 10×30 times a 30×5
dp[1][2]: only split k = 1
= dp[1][1] + dp[2][2] + dims[1]*dims[2]*dims[3]
= 0 + 0 + 30*5*60 = 9000 // (M₁ M₂): a 30×5 times a 5×60
Length 3. The full range [0..2]. Two possible split points:
split k = 0: (M₀)(M₁ M₂)
= dp[0][0] + dp[1][2] + dims[0]*dims[1]*dims[3]
= 0 + 9000 + 10*30*60 = 9000 + 18000 = 27000
split k = 1: (M₀ M₁)(M₂)
= dp[0][1] + dp[2][2] + dims[0]*dims[2]*dims[3]
= 1500 + 0 + 10*5*60 = 1500 + 3000 = 4500
We take the minimum: dp[0][2] = min(27000, 4500) = 4500, achieved by split k = 1, i.e., the parenthesization (M₀ M₁) M₂. That is more than 6× cheaper than the other order — exactly the kind of saving interval DP finds automatically.
The filled table (only the upper triangle is used since i ≤ j):
The answer is the top-right corner dp[0][2] = 4500. Notice the fill order: diagonal (length 1) first, then the super-diagonal (length 2), then the corner (length 3). Each cell only used cells below and to its left (smaller ranges) — which is precisely why increasing-length order works.
Code Examples¶
Example: Matrix Chain Multiplication (split-point interval DP)¶
Given dims of length n+1 (so there are n matrices), compute the minimum scalar multiplications.
Go¶
package main
import "fmt"
func matrixChain(dims []int) int {
n := len(dims) - 1 // number of matrices
// dp[i][j] = min cost to multiply matrices i..j
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
}
// length 1 => dp[i][i] = 0 (already zero-initialized)
for length := 2; length <= n; length++ {
for i := 0; i+length-1 < n; i++ {
j := i + length - 1
dp[i][j] = 1 << 62 // "infinity"
for k := i; k < j; k++ {
cost := dp[i][k] + dp[k+1][j] + dims[i]*dims[k+1]*dims[j+1]
if cost < dp[i][j] {
dp[i][j] = cost
}
}
}
}
return dp[0][n-1]
}
func main() {
dims := []int{10, 30, 5, 60}
fmt.Println("min scalar multiplications:", matrixChain(dims)) // 4500
}
Java¶
public class MatrixChain {
static int matrixChain(int[] dims) {
int n = dims.length - 1; // number of matrices
int[][] dp = new int[n][n];
// length 1 => dp[i][i] = 0
for (int length = 2; length <= n; length++) {
for (int i = 0; i + length - 1 < n; i++) {
int j = i + length - 1;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
int cost = dp[i][k] + dp[k + 1][j]
+ dims[i] * dims[k + 1] * dims[j + 1];
if (cost < dp[i][j]) dp[i][j] = cost;
}
}
}
return dp[0][n - 1];
}
public static void main(String[] args) {
int[] dims = {10, 30, 5, 60};
System.out.println("min scalar multiplications: " + matrixChain(dims)); // 4500
}
}
Python¶
def matrix_chain(dims):
n = len(dims) - 1 # number of matrices
dp = [[0] * n for _ in range(n)] # dp[i][i] = 0 base case
for length in range(2, n + 1):
for i in range(0, 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]
+ dims[i] * dims[k + 1] * dims[j + 1])
if cost < dp[i][j]:
dp[i][j] = cost
return dp[0][n - 1]
if __name__ == "__main__":
dims = [10, 30, 5, 60]
print("min scalar multiplications:", matrix_chain(dims)) # 4500
What it does: builds the dp table by increasing interval length and tries every split. Output is 4500, matching the hand walkthrough. Run: go run main.go | javac MatrixChain.java && java MatrixChain | python matrix_chain.py
Example: Burst Balloons (last-element interval DP)¶
Each balloon has a value; bursting balloon k earns nums[left] * nums[k] * nums[right] where left/right are its current neighbors. Maximize total coins. We pad with 1 on both ends and let dp[i][j] cover the open range (i, j).
Go¶
package main
import "fmt"
func maxCoins(nums []int) int {
// pad with 1 on both ends
a := make([]int, len(nums)+2)
a[0], a[len(a)-1] = 1, 1
copy(a[1:], nums)
n := len(a)
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
}
// length = distance between boundaries i and j (exclusive interior)
for length := 2; length < n; length++ {
for i := 0; i+length < n; i++ {
j := i + length
for k := i + 1; k < j; k++ { // k is burst LAST in (i, j)
coins := dp[i][k] + dp[k][j] + a[i]*a[k]*a[j]
if coins > dp[i][j] {
dp[i][j] = coins
}
}
}
}
return dp[0][n-1]
}
func main() {
fmt.Println("max coins:", maxCoins([]int{3, 1, 5, 8})) // 167
}
Java¶
public class BurstBalloons {
static int maxCoins(int[] nums) {
int n = nums.length + 2;
int[] a = new int[n];
a[0] = 1; a[n - 1] = 1;
for (int i = 0; i < nums.length; i++) a[i + 1] = nums[i];
int[][] dp = new int[n][n];
for (int length = 2; length < n; length++) {
for (int i = 0; i + length < n; i++) {
int j = i + length;
for (int k = i + 1; k < j; k++) { // k burst last
int coins = dp[i][k] + dp[k][j] + a[i] * a[k] * a[j];
if (coins > dp[i][j]) dp[i][j] = coins;
}
}
}
return dp[0][n - 1];
}
public static void main(String[] args) {
System.out.println("max coins: " + maxCoins(new int[]{3, 1, 5, 8})); // 167
}
}
Python¶
def max_coins(nums):
a = [1] + nums + [1] # pad both ends
n = len(a)
dp = [[0] * n for _ in range(n)]
for length in range(2, n): # distance between boundaries
for i in range(0, n - length):
j = i + length
for k in range(i + 1, j): # k is burst LAST in (i, j)
coins = dp[i][k] + dp[k][j] + a[i] * a[k] * a[j]
if coins > dp[i][j]:
dp[i][j] = coins
return dp[0][n - 1]
if __name__ == "__main__":
print("max coins:", max_coins([3, 1, 5, 8])) # 167
What it does: chooses, for each open range, which balloon to burst last so its neighbors are the stable boundaries. Output is 167. Run: go run main.go | javac BurstBalloons.java && java BurstBalloons | python burst.py
Coding Patterns¶
Pattern 1: Bottom-Up by Increasing Length (the canonical template)¶
Intent: Fill the dp table so every sub-range is ready before it's needed.
def interval_dp(n, base, combine):
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = base(i) # length-1 base case
for length in range(2, n + 1):
for i in range(0, n - length + 1):
j = i + length - 1
dp[i][j] = combine(dp, i, j) # tries all splits
return dp[0][n - 1]
Every interval DP is a fill-in of this skeleton; only base and combine change.
Pattern 2: Top-Down Memoization (recursion + cache)¶
Intent: Same recurrence, expressed as recursion that naturally only solves the ranges it needs.
from functools import lru_cache
def matrix_chain_memo(dims):
n = len(dims) - 1
@lru_cache(maxsize=None)
def solve(i, j):
if i == j:
return 0
best = float("inf")
for k in range(i, j):
best = min(best, solve(i, k) + solve(k + 1, j)
+ dims[i] * dims[k + 1] * dims[j + 1])
return best
return solve(0, n - 1)
Both styles are O(n³); bottom-up has lower constant factors and no recursion limit, top-down is often easier to write first.
Pattern 3: The Last-Operated-Element Trick¶
Intent: When an operation changes neighbors (bursting, removing), fix the last element so its neighbors are the stable boundaries i and j.
Pattern 4: Prefix Sums for O(1) Range Cost¶
Intent: For stone merging / optimal BST, the join cost is a range sum. Precompute prefix sums so sum(i..j) is O(1) instead of O(n).
def range_sum_setup(w):
prefix = [0]
for x in w:
prefix.append(prefix[-1] + x)
return lambda i, j: prefix[j + 1] - prefix[i] # sum of w[i..j]
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
| Wrong answer, off by one interval | Length loop started at the wrong value or j computed wrong. | Use j = i + length - 1 and start length at the correct base (1 or 2). |
dp[i][j] reads an uncomputed cell | Filling row-by-row instead of by increasing length. | Always iterate by interval length so sub-ranges are ready. |
| Always returns 0 | Forgot to initialize dp[i][j] to infinity before a min loop. | Set dp[i][j] = +∞ (min) or -∞ (max) before trying splits. |
| Index out of range | dims[j+1] or a[j] past the array end, or padding forgotten. | Pad balloons with sentinels; check dims has length n+1. |
| Stack overflow (top-down) | Deep recursion on large n without memoization. | Add the cache; or switch to bottom-up. |
| Overflow on large costs | Matrix products or sums exceed 32-bit int. | Use 64-bit (int64/long); in Python it's automatic. |
Performance Tips¶
- Precompute range sums with a prefix-sum array so
cost(i, k, j)isO(1); recomputing a sum inside thekloop turnsO(n³)intoO(n⁴). - Iterate splits cache-friendly — the inner
kloop readsdp[i][k](a row) anddp[k+1][j](a column); a flat 1D array indexedi*n + jimproves locality over arrays-of-arrays. - Use bottom-up over top-down for large
n: no recursion overhead, no function-call cost per state, and better cache behavior. - Store the split table only if you need reconstruction — otherwise the
split[i][j]array just wastes memory. - Consider Knuth / D&C optimization when the cost is well-behaved: they drop
O(n³)toO(n²)orO(n² log n). See siblings11and12.
Best Practices¶
- Decide split-point vs last-element framing first — write down what
dp[i][j]means in one sentence before coding. - Always fill by increasing interval length; it is the safest order and makes the dependency obvious.
- Initialize
dp[i][j]to the correct extreme (+∞for min,-∞or0for max) before the split loop. - Use prefix sums for any range-sum cost term.
- Write the base cases explicitly (length-1 ranges) even when they are zero — it documents intent.
- Test against a brute-force oracle (try all parenthesizations) on tiny inputs before trusting the DP on larger ones.
- Keep a parallel
split[i][j]table if the problem asks how (the actual parenthesization), not just the optimal value.
Edge Cases & Pitfalls¶
- Empty or single-element input —
dp[0][0]is the base case; with one matrix the answer is0. Handlen = 0andn = 1explicitly. - Padding for burst balloons — forgetting the two sentinel
1s on the ends gives wrong neighbor products at the boundaries. - Length loop start value — matrix chain starts at length 2 (length-1 is the base case
0); other problems may differ. - Open vs closed interval — burst balloons uses the open range
(i, j)withkstrictly between, while matrix chain uses the closed range[i..j]withkfromitoj-1. Mixing them up is a classic bug. - Split range bounds — for the split-point form,
krunsi .. j-1(so both halves are non-empty); for last-element,krunsi+1 .. j-1. - Integer overflow — products like
dims[i]*dims[k+1]*dims[j+1]can exceed 32 bits; use 64-bit. - This is for contiguous ranges only — if the subproblem is an arbitrary subset, interval DP does not apply; you likely need bitmask DP.
Common Mistakes¶
- Filling the table in the wrong order — iterating
ithenjrow-by-row reads cells that aren't computed yet. Always iterate by increasing length. - Forgetting to initialize
dp[i][j]to infinity — leaving it at0makes theminalways pick0, giving wrong (too-small) answers. - Off-by-one in the split loop —
kmust leave both halves non-empty;k in [i, j-1]for splits,k in (i, j)for last-element. - Recomputing range sums inside the inner loop — turns
O(n³)intoO(n⁴); use prefix sums. - Confusing the two framings — using a split-point recurrence for a problem (burst balloons) that needs the last-element trick gives a wrong answer, not just a slow one.
- Forgetting the balloon padding — boundary products break without the sentinel
1s. - Using
intwhere the cost overflows — silent wraparound to a wrong (often negative) value.
Cheat Sheet¶
| Question | Framing | Recurrence |
|---|---|---|
| Cheapest parenthesization of a chain | split point | dp[i][j] = min_k dp[i][k]+dp[k+1][j]+r_i r_{k+1} r_{j+1} |
| Min merge cost (stones) | split point | dp[i][j] = min_k dp[i][k]+dp[k+1][j]+sum(i..j) |
| Optimal BST | split point (root = k) | dp[i][j] = min_k dp[i][k-1]+dp[k+1][j]+freqSum(i..j) |
| Max coins (burst balloons) | last element | dp[i][j] = max_k dp[i][k]+dp[k][j]+a_i a_k a_j |
| Min cuts (palindrome partition) | split / last element | varies; see middle.md |
Core template:
for length = 1..n: # base length-1, then up
for i = 0..n-length:
j = i + length - 1
dp[i][j] = BEST_INIT # +inf for min, -inf/0 for max
for k in split_range(i, j):
dp[i][j] = best(dp[i][j],
dp[i][k] + dp[k(+1)][j] + cost(i,k,j))
answer = dp[0][n-1]
# baseline cost: O(n^3) time, O(n^2) space
Visual Animation¶
See
animation.htmlfor an interactive visualization.The animation demonstrates: - The
dp[i][j]table filled by increasing interval length (diagonal by diagonal). - For each cell, every candidate split pointkis tried and the optimum highlighted. - The two halvesdp[i][k]anddp[k+1][j]lighting up as they're combined with the join cost. - Step / Run / Reset controls to watch the table grow from the diagonal up to the final cornerdp[0][n-1].
Summary¶
Interval DP solves sequence problems by computing the best answer for every contiguous range [i..j] and building from short ranges to the whole sequence. The state is an interval; the transition chooses a split point (matrix chain: the outermost multiplication) or the last-operated element (burst balloons: which balloon to burst last). The defining recurrence is dp[i][j] = best over k of dp[i][k] + dp[k+1][j] + cost(i, k, j), and the table is filled by increasing interval length so sub-ranges are always ready. The baseline cost is O(n³) — O(n²) intervals times O(n) splits — with O(n²) space, comfortable up to a few hundred elements. The same template covers matrix chain, optimal BST, stone merging, palindrome partitioning, and removing boxes. When the cost function is well-behaved, Knuth's optimization (11) or the divide-and-conquer optimization (12) can cut the cubic factor down. Master the one template and the split-vs-last-element distinction, and a whole family of "what order should I combine things" problems becomes routine.
Further Reading¶
- Introduction to Algorithms (CLRS) — Chapter 15, matrix-chain multiplication and optimal binary search trees (the canonical interval DP exposition).
- Sibling topic
01-introduction-to-dp— optimal substructure and overlapping subproblems. - Sibling topic
02-memoization-vs-tabulation— top-down vs bottom-up styles. - Sibling topic
11-knuth-optimization— reducing interval DP fromO(n³)toO(n²). - Sibling topic
12-divide-and-conquer-optimization— another speedup for monotone optimal splits. - cp-algorithms.com — "Dynamic Programming on intervals" and "Knuth's optimization".
- LeetCode classics: Matrix Chain (LC 1130 variant), Burst Balloons (LC 312), Palindrome Partitioning II (LC 132), Stone Game variants, Remove Boxes (LC 546).