leetcode / 2-D dynamic programming / Interval DP advanced

Interval DP

Some problems are about a stretch of an array that shrinks from the ends or splits in the middle. Give every stretch (left, right) its own cell, fill the short stretches first, and each longer one is built from the pieces inside it. Bursting balloons and taking stones from either end are the classic cases.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Burst balloons: choose the last one
  5. Stone game: score difference
  6. When the range only shrinks on one side
  7. The general split recurrence
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Medium
  14. Hard
  15. Also uses this pattern
  16. Connections
  17. Read first
  18. Read next
  19. Check yourself
  20. References

Problem

The input is a row of things and the choices act on a contiguous range of it: take a stone from the left or right end, burst a balloon and let its neighbours close up, or split a range into two parts. A range of n items has about n²/2 sub-ranges, and each is described by just its two endpoints. If the answer for a range depends only on answers for ranges strictly inside it, you can solve all of them in order of increasing length, in O(n²) cells times the cost of choosing a split.

SignalPatternWhy
"Two players take from either end of a row"dp[l][r] = best score difference for whoever moves on l..rAfter a move the other player faces a smaller range, and the difference flips sign.
"Remove an item and its neighbours close the gap"Pick the item removed last in each rangeThe last item's neighbours are fixed (the range borders), which makes the two sides independent.
"Best way to split or parenthesise a sequence"dp[l][r] = best over split points k of dp[l][k] + dp[k][r] + costThe outermost split divides the range into two sub-ranges that can be solved separately.
A range that is only ever cut from its left end, with a parameterMemoize on (start, parameter) instead of (left, right)If the right edge never moves, one index plus a small state is enough.
Cost depends on neighbours that disappear as you workReverse the question to "what happens last"In reverse, the neighbours that matter are the range's boundaries, which do not change.

Intuition

A range is a smaller copy of the whole problem. If you take a stone from the left end of piles l..r, what remains is piles l+1..r, which is the same game on a shorter row. If you choose k as the last balloon to burst in the range (l, r), then before that final burst everything left of k and everything right of k was already gone, and those two sides never interacted because k stood between them the whole time. In both cases the bigger range reduces to smaller ranges.

The trick in balloons is that asking which balloon to burst first is a bad question: afterwards the neighbours change and the remaining problem is not a clean sub-range. Asking which to burst last is good: its neighbours are known (they are the borders of the range), and the two halves are independent sub-ranges of the same shape.

Because every range depends on strictly shorter ranges, fill by increasing length (the "gap" between the endpoints). The usual loop order is gap, then left, with right = left + gap. A plain row-by-row fill would try to read cells that are not yet computed.

Burst balloons table over padded positions The balloons 2, 4, 3, 5 are padded with a 1 on each side to 1, 2, 4, 3, 5, 1. Cell (l, r) holds the most coins from bursting everything strictly between positions l and r, with the best last balloon in small type. The top-right cell holds 115. 1 r = 0 1 0 2 r = 1 2 1 4 r = 2 4 2 3 r = 3 3 3 5 r = 4 5 4 1 r = 5 1 5 8 last: 2 30 last: 2 110 last: 2 115 last: 5 24 last: 4 100 last: 4 110 last: 5 60 last: 3 80 last: 5 15 last: 5

Only cells with at least one balloon strictly between the two endpoints are filled. Short ranges (near the diagonal) are computed first and feed the cells up and to the right. The blue corner is the whole row. The diagram scrolls sideways on a narrow screen.

Mechanics

Burst balloons: choose the last one

Pad the row with a 1 at each end, so a burst at the border never reads outside the array. Let coins[l][r] be the most you can earn by bursting every balloon strictly between positions l and r (neither l nor r is burst). If k is burst last in that range, the coins for it are pad[l] * pad[k] * pad[r], since everything else between l and r is already gone and only the borders remain as neighbours. Everything left of k, between l and k, was burst earlier and in isolation from the right side, and likewise on the right. So

coins[l][r] = max over k in (l, r) of coins[l][k] + pad[l]*pad[k]*pad[r] + coins[k][r].

Ranges with nothing strictly between (r = l + 1) are worth 0. The answer is coins[0][n + 1]. Correctness rests on the independence of the two sides: no balloon on the left of k ever has a neighbour on the right of k, since k is still present.

Stone game: score difference

Two players alternate, each taking the pile at either end; both play perfectly. Define diff[l][r] as the best value of (score of the player to move) minus (score of the other player) over piles l..r. Taking the left pile gains piles[l] and leaves the opponent to move on l+1..r, whose own advantage is diff[l+1][r], so that line is worth piles[l] - diff[l+1][r]. The right end is symmetric. A single pile is just its value:

diff[l][r] = max(piles[l] - diff[l+1][r], piles[r] - diff[l][r-1]).

The first player wins when diff[0][n-1] is positive. Since the two scores add to the total, the first player's score is (total + diff) / 2. Subtracting rather than tracking two scores is the key simplification, and it works because the game is zero-sum.

Score-difference table for the piles 3, 1, 4, 1, 5, 9 A triangle of cells, where cell (l, r) is the best score margin for the player to move on piles l to r. The diagonal holds the piles themselves. The top-right cell is 9, so the first player wins by 9. 3 3 1 1 4 4 1 1 5 5 9 9 3 2 2 5 0 9 1 3 -2 7 2 4 3 2 7 1 4 5 5 4 9

Row l, column r is the margin the mover can force on piles l to r. The amber diagonal is single piles; the blue corner is the whole row. Negative cells are ranges where the player to move is already losing.

When the range only shrinks on one side

In Stone Game II the player to move takes the first x piles of what is left, for any x from 1 to 2M, and M becomes the larger of M and x. The right end never moves, so a range is just a start index, and the state is (start, M). Let best(start, M) be the most stones the mover can collect from there. With suffix sums, taking x piles leaves the opponent at (start + x, max(M, x)), so best(start, M) = max over x of suffix[start] - best(start + x, max(M, x)), and if 2M covers everything left the mover simply takes it all. Memoize on the pair. There are O(n) starts and O(n) values of M, and each tries O(n) moves, so O(n³) in the worst case.

The general split recurrence

Matrix-chain multiplication, optimal binary search trees and minimum-cost triangulation share the shape cost[l][r] = min over k of cost[l][k] + cost[k][r] + join(l, k, r). Recognise it by the question "what is the last (or outermost) operation on this range, and does it cut the range into two independent pieces?".

Worked example

Balloons. For the row 2, 4, 3, 5, padded to 1, 2, 4, 3, 5, 1, the table fills out to 115 coins. The top row is 0, 0, 8, 30, 110, 115. One order that achieves it bursts the balloons at positions 2, 1, 0, 3 (counting from 0) in that sequence; the final one, the balloon worth 5, is alone between the two padded 1s and earns 5 coins.

Stone game. On the piles 3, 1, 4, 1, 5, 9 the table's top row is 3, 2, 2, 5, 0, 9. The first player can force a margin of 9. The total is 23, so the first player collects 16 and the second 7; the first player wins: yes.

i \ j012345
0322509
113-272
24327
3145
454
59

Stone Game II. With the same piles, the best the first player can collect when allowed to take 1 to 2M piles from the front is 16 stones out of 23. The memo holds only 4 entries, because most (start, M) pairs are either unreachable or end the game by taking everything left.

Implementation

def max_coins(nums):
    pad = [1] + nums + [1]
    n = len(pad)
    coins = [[0] * n for _ in range(n)]
    for gap in range(2, n):
        for left in range(n - gap):
            right = left + gap
            coins[left][right] = max(
                coins[left][k] + pad[left] * pad[k] * pad[right] + coins[k][right]
                for k in range(left + 1, right)
            )
    return coins[0][n - 1]


def first_player_margin(piles):
    n = len(piles)
    diff = piles[:]                      # diff for ranges of length 1
    for gap in range(1, n):
        for left in range(n - gap):
            right = left + gap
            diff[left] = max(piles[left] - diff[left + 1], piles[right] - diff[left])
    return diff[0]

Variations and edge cases

from functools import cache


def stone_game_two(piles):
    n = len(piles)
    suffix = [0] * (n + 1)
    for i in range(n - 1, -1, -1):
        suffix[i] = suffix[i + 1] + piles[i]

    @cache
    def best(start, m):
        if start + 2 * m >= n:
            return suffix[start]
        return max(suffix[start] - best(start + x, max(m, x)) for x in range(1, 2 * m + 1))

    return best(0, 1)


def min_merge_cost(sizes):
    prefix = [0]
    for s in sizes:
        prefix.append(prefix[-1] + s)
    n = len(sizes)
    cost = [[0] * n for _ in range(n)]
    for gap in range(1, n):
        for left in range(n - gap):
            right = left + gap
            cost[left][right] = min(cost[left][k] + cost[k + 1][right] for k in range(left, right)) + prefix[right + 1] - prefix[left]
    return cost[0][n - 1]
  • Loop order. Iterate by length (gap) first. Looping left from 0 upward and right outward often reads cells not yet computed.
  • Exclusive versus inclusive endpoints. Balloons use exclusive borders (l and r stay), stones use inclusive ones. Pick one convention and keep the base case consistent with it.
  • Padding. Without the two 1s, bursting an end balloon needs special cases. Padding removes them.
  • Empty or single input. A single balloon earns 1 * x * 1; an empty row earns 0, and max over an empty range would raise, so guard it or use default=0.
  • Rolling the table. The stone-game table can be reduced to one list as above because each length depends only on the previous length, but balloons need the full table since the split point reaches far.
  • Recursion depth. Memoized recursion over ranges may nest n deep; bottom-up avoids hitting Python's recursion limit.

Complexity and tradeoffs

  • Time: O(n²) cells. Stone-from-either-end does O(1) per cell, so O(n²) total; balloons try O(n) split points per cell, so O(n³). Stone Game II is O(n³) worst case.
  • Space: O(n²) for the table; the stone-game version can drop to O(n).
  • Baseline. Trying every burst order is n!; trying every move sequence in a two-player game is 2n.
  • Shortcut for stones. For the plain either-end game with an even number of piles, the first player never loses, by taking all odd-position or all even-position piles, and wins outright when the total is odd, as Stone Game guarantees; the DP is needed only when the margin or the variant matters.
  • When it fails. If the cost of a move depends on more than the range endpoints (for example on which items were removed earlier inside it), the state is too small. Then you may need a bitmask of removed items, or a different decomposition.

Problems

Burst Balloons is the one to master: it teaches "choose the last, not the first". The two Stone Game problems show the score-difference trick and the one-sided state.

Medium

  • 877. Stone Game NeetCode 250
    A range DP of the score difference the current player can force, choosing either end, solves it; parity also shows the first player always wins.
  • 1140. Stone Game II NeetCode 250
    Memoize on the start index and the current M, with suffix sums giving the stones still available.

Hard

  • 312. Burst Balloons NeetCode 150 NeetCode 250
    Choose which balloon bursts last within a range, since its neighbours are then the fixed range boundaries.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

No page builds on this one yet.

This builds directly on the prefix-pair tables from two-string DP, with both indices now living in the same sequence. The "last operation" view is the same one that makes divide and conquer work, and the score-difference trick for two players returns in game theory and minimax search.

Check yourself

Why does choosing the balloon to burst first not give a clean subproblem?

After it pops, its two neighbours become adjacent, and the coins for later bursts depend on that new adjacency. The remaining balloons are no longer an independent sub-range with fixed borders. Choosing the last one fixes the borders.

Why do we subtract diff[l+1][r] rather than add it in the stone game?

That cell is the margin for the opponent, who moves next. Whatever they gain over you reduces your margin. In the example the top corner is 9, built from such subtractions.

Why must the table be filled by increasing gap?

Each cell (l, r) reads cells strictly inside its range, which all have a smaller gap. Filling a longer gap first would read zeros that are not yet computed.

How many cells does the balloon table fill for 4 balloons?

Padding makes 6 positions, and a cell needs at least one position strictly between its endpoints, so there are 10 cells.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, chapter 14, matrix-chain multiplication and optimal binary search trees.
  • Knuth, D. E., "Optimum binary search trees", Acta Informatica 1, 1971, which speeds up the split search for a family of interval recurrences.
  • Skiena, S., The Algorithm Design Manual, chapter 10 on dynamic programming.
  • Kleinberg and Tardos, Algorithm Design, section 6.5 on RNA secondary structure, an interval DP.
  • Sedgewick and Wayne, Algorithms, section 4.4 (acyclic shortest paths): filling intervals shortest first is relaxing a DAG's vertices in topological order.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.