AnyLearn
All lessons

Designing a Dynamic Program: State, Transition, Order

Writing a dynamic program is three decisions, not a recurrence to memorise. This lesson works through choosing the state, deriving the running time from it, fixing the evaluation order, recovering the answer rather than its value, and why an O(nW) knapsack is not polynomial.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Three decisions and some bookkeeping

Every dynamic program is the same three decisions:

  1. The state. What arguments identify a subproblem?
  2. The transition. How is a state's value built from other states?
  3. The order. In what sequence can they be evaluated so dependencies are ready?

Everything else, the arrays, the loop bounds, the base cases, is bookkeeping that follows from those three.

Most people learn dynamic programming as a catalogue of recurrences: the knapsack one, the edit distance one, the coin change one. That does not transfer, because the next problem is not in the catalogue. Choosing a state does transfer. Once the state is right, the transition is usually forced, the order is read off the transition, and the running time falls out of both without any further analysis.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Three decisions and some bookkeeping

Every dynamic program is the same three decisions:

  1. The state. What arguments identify a subproblem?
  2. The transition. How is a state's value built from other states?
  3. The order. In what sequence can they be evaluated so dependencies are ready?

Everything else, the arrays, the loop bounds, the base cases, is bookkeeping that follows from those three.

Most people learn dynamic programming as a catalogue of recurrences: the knapsack one, the edit distance one, the coin change one. That does not transfer, because the next problem is not in the catalogue. Choosing a state does transfer. Once the state is right, the transition is usually forced, the order is read off the transition, and the running time falls out of both without any further analysis.

2. Choosing the state

The state must capture everything about the past that affects the future, and nothing else. That is the whole specification, and both halves bite.

Too little and the program is wrong, silently. The recurrence still runs and still returns a number; the number is just not the answer.

Too much and the program is slow. Every redundant field multiplies the state space, and the running time with it.

The practical test is a question you can ask out loud: given only this state, can I evaluate the transition without knowing how I arrived here? If the answer is no, something is missing. In edit distance the state is a pair of prefix lengths (i,j)(i,j), because how you aligned those prefixes never affects what remains. In knapsack it is the item index and the capacity still free. This is exactly the Markov property, transplanted from probability into algorithm design.

3. The running time is a multiplication

There is one formula, and it covers every dynamic program you will write:

time = (number of states) x (cost of one transition)

ProblemStatesPer transitionTime
Edit distancenmnmO(1)O(1), three optionsO(nm)O(nm)
0/1 knapsacknWnWO(1)O(1), take or skipO(nW)O(nW)
Matrix chain orderO(n2)O(n^2) pairsO(n)O(n) split pointsO(n3)O(n^3)
Held-Karp TSP2nn2^n \cdot nO(n)O(n) predecessorsO(2nn2)O(2^n n^2)

Read the table in the other direction and it becomes a design tool. If your state space is 2n2^n, no cleverness in the transition will rescue you; the state is what has to change. If the transition costs O(n)O(n) inside an O(n2)O(n^2) state space, that inner minimum is where an optimisation such as a monotone-split or convex-hull trick would pay.

4. Where a knapsack cell gets its value

The 0/1 knapsack transition is a two-way choice, and both branches read from the previous row only. That single observation drives two later decisions.

First, the evaluation order: fill rows in increasing ii and any cell in row ii has what it needs. Second, the space: if nothing ever reads row i2i-2, you do not need to keep it, and the whole table collapses to two rows, or with care to one.

The second option also silently encodes the constraint that makes this the 0/1 knapsack. Item ii contributes at most once because the branch that takes it drops to row i1i-1, where item ii is no longer available.

flowchart TD
A["dp(i, w): best value from first i items, capacity w"] --> B["Option 1: skip item i"]
A --> C["Option 2: take item i"]
B --> D["dp(i-1, w)"]
C --> E["dp(i-1, w - weight_i) + value_i"]
D --> F["Take the larger of the two"]
E --> F
F --> G["Both inputs live in row i-1"]

5. The knapsack, written out

Given items with weights and values and a capacity, maximise total value without exceeding capacity:

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        wt, val = weights[i - 1], values[i - 1]
        for w in range(capacity + 1):
            dp[i][w] = dp[i - 1][w]              # skip
            if wt <= w:                          # take, if it fits
                dp[i][w] = max(dp[i][w], dp[i - 1][w - wt] + val)
    return dp[n][capacity]

The two loops enumerate the states, the body is the transition, and the loop order is the evaluation order. Nothing in this code is specific to knapsack except the three lines in the middle, which is the point: the shape is reusable, the transition is the problem.

6. A gotcha hiding in one loop direction

Since every cell reads only row i1i-1, you can drop to a single array. But now the direction of the inner loop carries meaning, and getting it wrong does not produce an error. It produces a different problem's answer.

for i in range(n):
    for w in range(capacity, weights[i] - 1, -1):   # DESCENDING
        dp[w] = max(dp[w], dp[w - weights[i]] + values[i])

Going downward, dp[w - weights[i]] has not yet been touched this round, so it still holds the row i1i-1 value and item ii is used at most once. That is 0/1 knapsack.

Going upward, that same cell was already updated this round, so it may already include item ii, and the item gets reused freely. That is the unbounded knapsack.

One keystroke separates two different problems, both of which run and return plausible numbers.

7. Recovering the solution, not just its value

The table gives you a number. Usually you want the artefact: which items, which alignment, which split.

Two standard approaches. Store a parent pointer alongside each cell recording which branch won, then follow pointers back from the final state. Or store nothing and re-derive it, walking backwards from (n,W)(n, W) and asking at each cell whether it equals dp[i-1][w]; if not, item ii must have been taken.

This collides directly with the space optimisation. Rolling to one array leaves you the optimum's value and no way to reconstruct it. For sequence alignment the tension is resolved: Hirschberg's algorithm, published in Communications of the ACM in 1975, recovers the full alignment in O(min(n,m))O(\min(n,m)) space and still O(nm)O(nm) time, by recursively splitting on the middle column and solving each half. You pay a constant factor in time, not an asymptotic one.

8. Why O(nW) is not a polynomial-time algorithm

The knapsack table has nWnW cells, so the algorithm is O(nW)O(nW), and knapsack is NP-hard. Those two statements are both true and do not conflict.

Complexity is measured against the length of the input, in bits. The capacity WW is a single number, written in about log2W\log_2 W bits. So a capacity of one billion adds roughly 30 bits of input while multiplying the work by a billion. The running time is exponential in the input length, and O(nW)O(nW) is called pseudo-polynomial.

The consequence is a sharp performance cliff that has nothing to do with the number of items. Knapsack with 50 items and capacity 1,000 is instant. Fifty items and a capacity of 101210^{12} is hopeless, even though the input barely grew. Problems like this, NP-hard but pseudo-polynomially solvable, are called weakly NP-hard.

9. The failure mode: a state that is not enough

Suppose the knapsack gains one constraint: take at most kk items. The natural move is to keep the same table and check the count at the end. That is wrong, and it fails quietly.

The state (i,w)(i, w) records the best value for a prefix and a capacity, but two different item sets can reach the same cell with the same value and different cardinalities. The recurrence keeps whichever has more value and discards the count, so by the time you check, the information is gone.

The fix is to put the missing quantity into the state: (i,w,j)(i, w, j), where jj is items used so far. The table grows by a factor of kk and the running time with it, which is the honest price of the extra constraint.

The general symptom is worth memorising: a dynamic program whose state is too small does not crash, it lies.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What is the correct test for whether a proposed DP state is sufficient?
    • It contains every input variable mentioned in the problem statement
    • The transition can be evaluated from the state alone, without knowing how that state was reached
    • The state space is polynomial in the input size
    • Each state is reachable from the base case by exactly one path
  2. Matrix chain ordering has O(n^2) states and minimises over O(n) split points per state. What is its running time?
    • O(n^2), since the split search is amortised away
    • O(n^2 log n)
    • O(n^3)
    • O(2^n), because the number of parenthesisations is exponential
  3. In the one-array knapsack, what happens if the inner capacity loop runs upward instead of downward?
    • It computes the unbounded knapsack, allowing each item to be reused
    • It raises an index error when the capacity drops below the item weight
    • It returns the same answer but uses more memory
    • It computes the fractional knapsack instead
  4. Why is an O(nW) knapsack algorithm not a polynomial-time algorithm?
    • Because the constant factor hidden by big-O notation grows with W
    • Because building the table requires more memory than a polynomial algorithm may use
    • Because W is not known until the input has been fully read
    • Because W is written in about log W bits, so the running time is exponential in the input length
  5. You add a 'take at most k items' constraint to knapsack and check the count after filling the usual table. What goes wrong?
    • The table overflows because k is not bounded by the capacity
    • The recurrence discards item counts when comparing values, so the count is unavailable at the end
    • Nothing, provided k is at most the number of items
    • The evaluation order becomes cyclic and the table cannot be filled

Related lessons

Computer Science
advanced

Conditioning and Stability: Whose Fault Is the Error

Two different things can make a numerical answer inaccurate: the problem may amplify any input perturbation, or the algorithm may introduce its own. They are separate quantities, one belongs to the problem and one to the code, and only the second is fixable. This lesson separates them and shows what follows.

8 steps·~12 min
Computer Science
advanced

Cancellation: Where the Digits Actually Go

Every operation is correctly rounded, yet results can be wrong in the first digit. The reason is that subtracting nearly equal numbers is exact and still catastrophic: it does not create error, it promotes error that was already there. This lesson builds that mechanism and the summation techniques that recover the lost accuracy.

8 steps·~12 min
Computer Science
advanced

Count-Min, and Why Sketches Compose

The count-min sketch estimates how often an item occurred using a fixed grid of counters, and it never underestimates. This lesson builds it, states the error bound that makes it usable, shows where it is useless, and ends on the property shared by all three structures that explains why they run distributed systems.

8 steps·~12 min
Computer Science
advanced

HyperLogLog: Counting Distinct Items in Kilobytes

Counting distinct items exactly needs memory proportional to the count. HyperLogLog answers the same question in a fixed twelve kilobytes, for cardinalities into the billions, by measuring an improbable event rather than storing anything. This lesson builds that idea from the leading-zeros intuition up.

8 steps·~12 min