Facebook Pixel

Interval DP Introduction

Prerequisite: Dynamic Programming Introduction

Interval DP solves problems on subarrays or substrings where the answer for a range [l, r] depends on smaller ranges within it. The state dp[l][r] represents the answer for the subproblem covering indices l through r inclusive.

Identifying Interval DP Problems

Two signals indicate this pattern. First, the input is a linear sequence (array or string). Second, the problem requires considering all possible contiguous ranges, and solutions for larger ranges combine solutions from smaller ranges.

Common interval DP problems include matrix chain multiplication, optimal game strategies, palindrome partitioning, and merging stones. Constraints typically allow sequence lengths up to 500-1000, suggesting an O(n^2) or O(n^3) solution.

Example: Coin Game

Two players take turns picking coins from either end of a row. Each player plays optimally to maximize their score. Find the maximum score the first player can achieve.

State: dp[l][r] = maximum score difference (current player minus opponent) for coins in range [l, r]

Transition: The current player picks from either end, then the opponent plays optimally on the remaining range:

dp[l][r] = max(coins[l] - dp[l+1][r], coins[r] - dp[l][r-1])

The subtraction accounts for alternating turns: what's good for the opponent is bad for the current player.

Base case: dp[i][i] = coins[i] (single coin: take it)

Common Transition Patterns

Interval DP transitions typically examine smaller ranges within the current range. The two main patterns are element removal and interval splitting.

Element removal shrinks the range from one end. In the coin game, picking the left coin reduces [l, r] to [l+1, r]. Picking the right coin reduces it to [l, r-1].

Interval splitting divides the range at some point m. For matrix chain multiplication, dp[l][r] tries all split points: dp[l][m] + dp[m+1][r] + cost(l, m, r). This requires iterating over all possible m values, adding an O(n) factor to each state.

Top-Down: The Natural Choice for Interval DP

Interval DP is one case where top-down is often easier than bottom-up. The reason: you don't need to figure out the processing order.

With bottom-up, you must ensure smaller intervals are computed before larger ones. This requires the non-obvious "iterate by length" pattern. With top-down, the recursion handles dependencies automatically—when solve(l, r) needs solve(l+1, r), it just calls the function, which computes and caches the result.

Top-Down Recursion

from functools import lru_cache

def coin_game(coins):
    @lru_cache(maxsize=None)
    def solve(l, r):
        if l == r:
            return coins[l]  # Base case: take the only coin
        pick_left = coins[l] - solve(l + 1, r)
        pick_right = coins[r] - solve(l, r - 1)
        return max(pick_left, pick_right)

    return solve(0, len(coins) - 1)

The code reads exactly like the recurrence. No index manipulation, no worrying about which intervals to compute first.

Bottom-Up: Processing by Interval Length

If you prefer bottom-up or need to avoid recursion, you need to understand the state space and fill order.

The Two-Dimensional State Space

Interval DP State Space

Unlike prefix-based DP where dp[i] considers elements 0..i, interval DP uses dp[l][r] to represent any contiguous range. For a sequence of length n, there are O(n^2) possible intervals, though only ranges where l <= r are valid (the upper triangle of the table).

Fill Order: By Increasing Length

Since larger intervals depend on smaller ones, process intervals in order of increasing length. Start with length 1 (base cases), then length 2, then length 3, up to length n. Within each length, iterate over all valid starting positions.

def coin_game(coins):
    n = len(coins)
    dp = [[0] * n for _ in range(n)]

    # Base case: single coins (length 1)
    for i in range(n):
        dp[i][i] = coins[i]

    # Fill by increasing length
    for length in range(2, n + 1):
        for l in range(n - length + 1):
            r = l + length - 1
            pick_left = coins[l] - dp[l + 1][r]
            pick_right = coins[r] - dp[l][r - 1]
            dp[l][r] = max(pick_left, pick_right)

    return dp[0][n - 1]

This guarantees that when computing dp[l][r], all smaller intervals like dp[l+1][r] and dp[l][r-1] are already computed.

Time and Space Complexity

Interval DP uses O(n^2) space for the 2D table. Time complexity depends on the transition. Element removal (constant work per state) yields O(n^2) total. Interval splitting (trying all split points) yields O(n^3) total.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro