Theory · Dynamic Programming
DSA Roadmap Open Visualizer Back to intro

Algocraft · Theory

Dynamic Programming, from first principles

Before you touch a single visualizer, the goal here is to build the mental model: what makes a problem a "DP problem," the two properties that make it solvable this way, the two ways to actually implement it, and a repeatable framework you can apply to a problem you've never seen before. Diagrams and a worked example throughout — no memorizing required.

01 · Foundation

Why does DP exist at all?

Take the classic recursive Fibonacci function: fib(n) = fib(n-1) + fib(n-2). It's correct, but if you actually draw out every call it makes, something wasteful shows up immediately — the same smaller subproblem gets solved again and again from scratch.

Below is the full call tree for fib(4). Notice fib(2) appears twice, as two completely separate branches that each redo the same work:

fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) recomputed ↑ ↓ same subproblem

Two separate branches both solve fib(2) from scratch (orange). For fib(4) that's one wasted call — for fib(40) it's billions.

Dynamic Programming is just: solve each distinct subproblem once, save the answer, and reuse it instead of recomputing it. That's the entire idea. Everything else — tables, memo dictionaries, recurrence relations — is machinery built to support that one trick.

02 · Recognizing DP

The two properties every DP problem has

A problem is worth solving with DP only when it has both of these. Miss one and DP either won't help or won't even be correct.

Quick self-test: can you describe the answer to the big problem purely in terms of answers to smaller versions of itself? If yes to both bullets above, it's a DP problem.

03 · Implementation

Two ways to apply it: Top-Down vs Bottom-Up

Same idea, opposite direction. Top-down starts from the big question and recurses downward, caching as it goes. Bottom-up starts from the smallest subproblems and builds upward until it reaches the answer you actually wanted.

🔽 Top-Down (Memoization)

  • Write the natural recursive solution first
  • Add a cache (array/dict) keyed by the subproblem's state
  • Before recursing, check the cache — return immediately if already solved
  • Reads like the recurrence relation, easy to get right first
memo = {}
def solve(state):
    if state in base_cases:
        return base_case_value
    if state in memo:
        return memo[state]
    memo[state] = combine(
        solve(smaller_state_1),
        solve(smaller_state_2)
    )
    return memo[state]

🔼 Bottom-Up (Tabulation)

  • Build a table sized to hold every subproblem's answer
  • Fill in the base cases first
  • Iterate in an order where every value you need is already filled
  • No recursion — no call-stack depth to worry about
dp = [base_case] * (n + 1)
for i in range(1, n + 1):
    dp[i] = combine(
        dp[i - 1],
        dp[i - 2]
    )
return dp[n]

Neither is "more correct" — bottom-up is usually a little faster (no call-stack overhead) and easier to space-optimize; top-down is usually faster to write because it mirrors how you'd explain the recurrence out loud. Every visualizer on this site lets you toggle between both so you can watch the same problem solved both ways.

04 · Method

A repeatable framework for any DP problem

This is the actual sequence to work through, in order, whether the problem is Fibonacci or something you've never seen before.

  1. 1 Define the stateWhat minimal set of variables uniquely describes a subproblem? For Coin Change it's just "remaining amount." For LCS it's "how far along string A and string B."
  2. 2 Write the recurrenceHow does the answer for a state relate to answers for smaller states? This is the "optimal substructure" step made concrete — usually a min/max/sum over a couple of choices.
  3. 3 Identify the base casesThe smallest states you can answer directly, with no recursion — amount 0 needs 0 coins, an empty string has 0 common subsequence, etc.
  4. 4 Decide computation orderIf going bottom-up: make sure every dp[i] you fill only depends on dp[j] values that are already filled by the time you get there.
  5. 5 Optimize space (optional)If dp[i] only ever depends on the last one or two rows/values, you often don't need the full table — a couple of rolling variables is enough.

05 · Patterns

Common DP patterns you'll keep seeing

Most interview DP problems are a variation on a small number of underlying shapes. Recognize the shape, and half the problem is already solved. Each card links to the matching interactive visualizer.

06 · Worked example

Coin Change, one array cell at a time

Coins [1, 2, 5], target amount 6. State: dp[a] = fewest coins needed to make amount a. Recurrence: dp[a] = min(dp[a-c] + 1) over every coin c ≤ a. Base case: dp[0] = 0 (zero coins needed to make zero).

amount0123456
dp[a] 0 1 1 2 2 1 2

dp[6] (outlined) checks all three coins: dp[5]+1 = 2, dp[4]+1 = 3, dp[1]+1 = 2 — the smallest is 2, meaning amount 6 is reachable in 2 coins (a 5 and a 1). Every earlier cell was filled the exact same way, only reaching back to cells that were already solved — which is only safe because we filled left-to-right, so dp[a-c] is always already computed by the time we need it.

Open the Coin Change visualizer to watch this table fill animate step by step, in both Bottom-Up and Top-Down mode.

07 · Complexity

How to reason about time & space

The fast way to estimate a DP solution's time complexity, without tracing every call: (number of distinct states) × (work done per state).

Space follows the same logic — one memo/table slot per state, unless you notice each state only ever needs the last row (or last couple of values), in which case you can drop a 2D table down to one rolling row, or a 1D array down to a couple of variables.

08 · Before you start coding

Common mistakes & how to spot a DP problem