leetcode / 1-D dynamic programming / Dynamic programming fundamentals intro

Dynamic programming fundamentals

Dynamic programming is recursion that refuses to repeat itself. When a problem splits into smaller versions of itself and those versions overlap, you solve each distinct one once and reuse the answer. The whole craft is choosing the right "state", the small description that fully captures a subproblem.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The recipe
  5. Top-down and bottom-up
  6. Why it is correct
  7. Why not just be greedy?
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Also uses this pattern
  14. Connections
  15. Read first
  16. Read next
  17. Check yourself
  18. References

Problem

Plain recursion on some problems takes exponential time, not because the problem is hard but because the same sub-question is asked again and again. Counting ways, finding a best total or deciding whether something is possible, where every choice leaves a smaller problem of the same kind, fits this shape. Dynamic programming (DP) removes the repetition, turning exponential work into a number of steps proportional to the number of distinct sub-questions.

SignalPatternWhy
"Number of ways to ..."DP that sums over the last choiceWays to finish = the sum of ways after each possible first or last move.
"Minimum / maximum total of ..."DP that takes a min or max over choicesThe best total after a choice depends only on the state it leads to.
"Is it possible to reach / form ...?"DP over booleansTrue if any move leads to a true state.
Brute-force recursion repeats the same argumentsMemoize on those argumentsOverlapping subproblems are the defining sign.
Choices look locally best but failDP, not greedyDP weighs every option; greedy commits to one without proof.

Intuition

Take the Fibonacci numbers, where each is the sum of the two before it. Written as plain recursion, fib(k) calls fib(k - 1) and fib(k - 2). Computing fib(7) makes 41 calls, yet only 8 different arguments exist: fib(1) alone is computed 13 times. For fib(30) the same recursion makes over two and a half million calls (2692537), still only 31 different questions.

Two ideas fix it, and every DP solution uses one of them. Memoization keeps the recursion and adds a cache: before computing a state, check whether you already have it. Tabulation drops the recursion and fills a table of answers in an order where everything a cell needs is already filled. They compute the same values.

How many times plain recursion computes each Fibonacci value A bar for each argument from 7 down to 0 giving the number of calls made while computing fib(7): fib(7) is called 1 time; fib(6) is called 1 time; fib(5) is called 2 times; fib(4) is called 3 times; fib(3) is called 5 times; fib(2) is called 8 times; fib(1) is called 13 times; fib(0) is called 8 times. That is 41 calls in total, against 13 with a cache. calls to fib(k) in plain recursion for fib(7) fib(7) 1 fib(6) 1 fib(5) 2 fib(4) 3 fib(3) 5 fib(2) 8 fib(1) 13 fib(0) 8

Amber bars are repeated work. The diagram scrolls sideways on a narrow screen.

Mechanics

The recipe

  1. State. What few numbers pin down a subproblem? (An index, a remaining amount, a pair of positions.) If two different histories lead to the same numbers and the future is identical, they are the same state.
  2. Meaning. Say in one sentence what dp[state] stores, for instance "the cheapest cost to finish from here". Most wrong DPs fail here, not in the code.
  3. Transition. Express dp[state] using states that are smaller. Try every choice available at this state and combine the results with min, max or a sum.
  4. Base cases. The states with no smaller state: the empty input, the last cell.
  5. Order. Fill states so that each one's dependencies come first. Recursion with a cache handles this for you; a table needs it worked out.
  6. Answer. Which state, or which combination of states, is the question?

Top-down and bottom-up

Top-down is the brute-force recursion with @functools.cache. It computes only the states reachable from the question, and it is the easy way to get a correct solution first. Bottom-up is a loop over a table. It avoids recursion depth limits and call overhead, and lets you see when old rows can be thrown away. In Python, both are accepted; start with top-down if the order of filling is unclear.

Why it is correct

The method relies on optimal substructure: the best answer for a state is built from best answers to smaller states, because what happens after a choice depends only on the state it leads to, not on how you got there. If the future depended on the history, the history would belong in the state. The running time is then the number of states times the work per state, since each state is solved exactly once.

Why not just be greedy?

A greedy rule commits to the locally best choice and never looks back. That works only when you can prove it. DP is the safe default because it compares every choice, and its cost is the size of the state space. The triangle below is a case where greedy loses.

A number triangle with the cheapest finish from every cell A triangle of numbers with rows 5; 7, 3; 2, 8, 6; 9, 1, 4, 7. Each cell also shows the cheapest total from that cell down to the bottom, working from the bottom row upward. The cheapest path from the top has total 15, through the values 5, 7, 2, 1. A greedy walk that always takes the smaller child gets 18. 5 →15 7 →10 3 →12 2 →3 8 →9 6 →10 9 →9 1 →1 4 →4 7 →7

Circle: the cell's own number. Arrow figure: the cheapest total from it to the bottom. Blue cells are the cheapest path.

Worked example

Fibonacci, three ways. The plain recursion makes 41 calls for fib(7). With a cache it makes 13 calls and computes 6 states, still returning 13. The table 0, 1, 1, 2, 3, 5, 8, 13 gets the same value from one loop over 8 cells.

kCalls to fib(k), plain recursionTable cell k
080
1131
281
352
433
525
618
7113

Cheapest path down a triangle. From a cell you may go to the cell directly below or the one below and to the right. State: the cell. Meaning: the cheapest total from this cell to the bottom row. Transition: its own number plus the smaller of the two cells below. Base: the bottom row is its own number. The order is bottom row upward, and the answer is the top cell.

Filling from the bottom gives the arrow figures in the diagram, and the top cell holds 15. Following the smaller figure at each step recovers the path 5 then 7 then 2 then 1. A greedy walk taking the smaller of the two children each time ends at 18, because the child that looks cheaper hides a worse finish.

Implementation

from functools import cache

def fib(n):
    @cache
    def go(k):
        if k < 2:
            return k
        return go(k - 1) + go(k - 2)
    return go(n)


def min_triangle_path(triangle):
    best = list(triangle[-1])            # bottom row
    for row in reversed(triangle[:-1]):
        best = [row[j] + min(best[j], best[j + 1]) for j in range(len(row))]
    return best[0]

The triangle solution keeps only one row of answers, since each row needs just the one below it. That is the usual space trim for DP: keep as many previous rows as the transition reads.

Variations and edge cases

def fib_iter(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a
  • Rolling variables. If dp[i] reads only the previous one or two entries, keep two variables instead of an array (as in fib_iter).
  • Reconstruction. To print the choice, not just its value, keep the table and walk it forward, or store the chosen move beside each value.
  • Dictionary state. If a state is not a small integer (a tuple, a string), memoize on a dictionary; @cache does so for you.
  • Recursion depth. Top-down on a long input can exceed Python's recursion limit (1000 by default). Go bottom-up, or raise the limit.
  • Mutable arguments. Never memoize on a list; convert to a tuple or use indices.
  • Edge cases: empty input, a single element, and the base cases written for the wrong offset, which is the most common slip.

Complexity and tradeoffs

  • Time: the number of states times the work to evaluate one. Fibonacci has n states with O(1) work, so O(n); the triangle has about n²/2 states, also O(1) each.
  • Space: the table, or the cache; often reducible to the last few rows or entries.
  • Baseline. The unmemoized recursion is exponential here: 41 calls for n = 7, roughly 1.6 times more for each step up.
  • When it fails. If the number of states is itself enormous (values up to a billion, or subsets of many items), the table is too big. Look for a greedy proof, a closed form or a smaller state. If the future depends on the whole history, the state is not capturing it.

Problems

No list problem has this page as its home, because it supplies the method. The problems listed below use the same recipe: Climbing Stairs, House Robber and Decode Ways in the linear DP page, Coin Change as knapsack, and Longest Common Subsequence, Edit Distance and Regular Expression Matching as string DP. Each is one use of "name the state, write the transition, fill in order".

No list problem has this page as its home: it teaches the vocabulary every other page uses.

Also uses this pattern

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

Connections

Read first

Read next

DP is recursion plus a cache, so the call-tree reading of recursion carries over directly. Where a recurrence picks among options it relates to backtracking, which explores all choices without sharing results, and to greedy, which explores one. The state-space size you count here is the same quantity as the time bound.

Check yourself

Why does plain recursion for Fibonacci take exponential time?

The same sub-calls repeat. For fib(7), fib(1) alone is called 13 times, and the total is 41 calls for 8 distinct arguments.

What makes a state sufficient?

Two histories that reach the same state must have exactly the same set of possible futures and the same best future value. If something from the history changes the future, that thing belongs in the state.

On the triangle, why does the greedy walk give 18 rather than 15?

It chooses the smaller child at each step, but a small child can lead to expensive cells below. The DP compares the cheapest finish from each child, not the child's own number.

When can you replace the table with a few variables?

When each entry reads only a fixed number of earlier entries. Fibonacci reads the last two, so two variables are enough; the triangle reads one row, so one row is enough.

References

  • Bellman, Dynamic Programming, Princeton University Press, 1957.
  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, chapter 14 (dynamic programming; optimal substructure and overlapping subproblems).
  • Skiena, The Algorithm Design Manual, chapter 10 (dynamic programming).
  • Sedgewick and Wayne, Algorithms, section 4.4 (acyclic shortest paths): relaxing a DAG's vertices in topological order is the same move as filling a DP table in dependency order.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.