skip to content

In interval DP, why can't the dp[i][j] table be filled in plain row-major order?

level: middleimportance: should knowfreq 45%

answer

  1. Which entries does dp[i][j] read?
  2. Are those ranges longer or shorter?
  3. Row-major reaches dp[k][j] too late
  4. Order the fill by span, not index
  5. Or run the left endpoint downward

basics

~20 s

An interval DP entry dp[i][j] is built from dp[i][k] and dp[k][j] for split points i < k < j, which are always shorter subranges. Row-major order reaches dp[k][j] before that row is written, so fill by increasing interval length instead.

solid answer

~40 s

In interval DP the state is a subrange and the transition is `dp[i][j] = w(i,j) + min over i < k < j of (dp[i][k] + dp[k][j])`. Both operands span strictly fewer positions than `[i, j]`, so the dependency order is by **interval length**, not by index. A plain outer loop over `i` ascending with `j` ascending reads `dp[k][j]` with `k > i` — a row further down that has not been written yet, so it still holds the initial value and the answer is silently wrong rather than crashing. The safe orders are: outer loop over the span `j - i` from smallest to largest, or outer loop over `i` descending with `j` ascending. Base cases are the intervals too short to contain any split point.

code

pseudocode · 11 lines
pseudocode
// dp[i][j] = best cost for the range between positions i and j
// x[] holds the positions; w(i, j) = x[j] - x[i]
for i in 0..n-2
    dp[i][i+1] = 0            // base: no split point fits inside

for i in 0..n-2               // BUG: ascending left endpoint
    for j in i+2..n-1
        best = INFINITY
        for k in i+1..j-1
            best = min(best, dp[i][k] + dp[k][j])   // dp[k][j], k > i
        dp[i][j] = best + w(i, j)

go deeper

for a junior

Know that dp[i][j] here stands for a contiguous subrange, not a prefix, and that its value is assembled from two shorter subranges. Recall that the loop is written over interval length.

for a middle

Be ready to derive the fill order from the recurrence on the spot: name the two entries the transition reads, show that both cover shorter spans, and conclude that span-ascending or left-endpoint-descending are the valid orders.

for a senior

Expect to be asked what a wrong fill order actually produces. Show that it fails silently with a plausible number, that tiny hand-checks pass by accident, and describe how you would catch it — a brute-force cross-check on inputs of size five to eight.

for a principal

Own the general principle: a recurrence is only a valid dynamic program if its transitions induce a partial order on states. Push a team to state that order explicitly for every DP they write, since an unstated order is where silent wrong answers live.

## The state and where its dependencies live Interval DP solves problems whose subproblem is *a contiguous subrange of a sequence*, not a prefix. The table entry is `dp[i][j]` = the best cost for the piece of the input between positions `i` and `j`. The transition picks a split point `k` strictly inside the range and combines the two halves: ``` dp[i][j] = w(i, j) + min over i < k < j of ( dp[i][k] + dp[k][j] ) ``` where `w(i, j)` is the cost charged for combining (or separating) the whole range once. Every operand on the right — `dp[i][k]` and `dp[k][j]` — covers a **strictly shorter** interval than `[i, j]`, because `i < k < j`. That single fact fixes the entire fill order. ## Why row-major order breaks A linear DP over prefixes has all its dependencies to the left, so the obvious ascending loop works. Interval DP does not. Consider the ascending nest: outer `i` from 0 upward, inner `j` from `i+1` upward. When you compute `dp[i][j]` you need `dp[k][j]` for every `k` between `i` and `j`. Those entries sit in **rows below the current row** — larger first index — and the outer loop has not reached them. They still contain whatever the table was initialized to. This is a nasty failure mode, and it is the reason the question gets asked. Nothing throws. If the table was initialized to a large sentinel, the minimum silently ignores every legitimate split that needed a lower row and you get a too-large answer; if it was initialized to zero, you get a too-small answer that looks plausible on tiny inputs. Small hand-checked cases of size three or four often pass by accident, because with so few positions the missing entries are base cases. The bug shows up only at size five and above. ## The two orders that do work **By interval length (the canonical one).** Iterate `span` from the smallest non-trivial length up to `n-1`; for each `span`, let `i` run over all valid left endpoints and set `j = i + span`. Every entry read has a smaller span and is therefore already final. This order is self-documenting: it makes the dependency structure — shorter intervals first — visible in the loop header, which is why it is the one you should write on a whiteboard. **By decreasing left endpoint.** Iterate `i` from `n-1` down to 0 and `j` from `i+1` up to `n-1`. Now `dp[k][j]` with `k > i` lives in an already-completed row, and `dp[i][k]` with `k < j` lives earlier in the current row. Also correct, marginally friendlier to sequential memory access, and harder for a reviewer to verify at a glance. A third route sidesteps the question entirely: **top-down memoized recursion**. Recursion evaluates dependencies on demand, so you never have to reason about an explicit order — you pay a recursion stack of depth proportional to the interval nesting and a hash-or-array lookup per call instead. The state count and the transition count are unchanged; only the constant factor and the space profile move. ## Base cases The base cases are exactly the intervals with **no valid split point** — the ranges so short that no `k` satisfies `i < k < j`. Depending on how the indices are defined (positions between marks, or elements of a chain), that is the intervals of length one or two. Getting this off by one is the other classic error here: if you leave the shortest intervals uninitialized, the minimum over an empty set of split points leaves a sentinel that then poisons every longer interval that reads it. ## Why this matters beyond the loop nest The fill order is not bookkeeping; it *is* the statement that the recurrence is well-founded. If you cannot name an ordering in which every entry's dependencies precede it, the recurrence has a cycle and is not a valid dynamic program at all. Stating "shorter intervals first" out loud is how you demonstrate to an interviewer that you have checked that property rather than pattern-matched a loop nest from memory. A useful sanity check before writing any code: name the state, name the transition, then name the partial order the transition induces on states. For prefix DP that order is index; for subset DP it is subset size; for interval DP it is interval length. The loop nest is just that order written down.

  • Is there any correct fill order other than iterating by interval length?
    Yes. Running the left endpoint `i` from the last position down to the first, with `j` ascending inside, is also correct: `dp[k][j]` for `k > i` belongs to an already-finished row and `dp[i][k]` for `k < j` was written earlier in the current row. Top-down memoized recursion avoids the question altogether by resolving dependencies on demand, at the cost of stack depth. Iterating by span is preferred in interviews because it makes the shorter-first dependency visible.
  • If you fill in the wrong order, what does the output look like?
    Not a crash — a wrong number. Missing entries still hold their initialization, so with a large sentinel the minimum skips legitimate splits and the answer comes out too high; with zeros it comes out too low. Inputs of size three or four often pass anyway, because the entries being read early happen to be base cases. The discrepancy appears at size five and up, which is why hand-testing a tiny case is not enough here.
  • Which entries are the base cases, and what happens if you skip them?
    The base cases are the intervals too short to contain any split point — no `k` satisfies `i < k < j`. If you leave them uninitialized, the minimum over an empty set of splits leaves a sentinel in place, and every longer interval that reads that cell inherits it. One uninitialized short interval corrupts a whole diagonal of the table.

Building a wall of arches: every wide arch rests on two narrower ones, so you lay all the narrow spans before any wide span, whatever order you walk along the wall.

saying these in an interview costs you the question

  • Fills the table with ascending i and ascending j
  • Says the order does not matter because it is a table
  • Thinks the table converges if you sweep it repeatedly
  • Forgets to initialize the shortest intervals
  • Claims dependencies are always to the left, as in prefix DP

context