LearnAbout

DP Fundamentals (1D)

On this page
Quick reference
Fibonacci — naive recursiveO(2ⁿ)
Fibonacci — memoized (top-down)O(n)
Fibonacci — tabulated, rolling variablesO(n)
Climbing Stairs / House Robber — full tableO(n)
Climbing Stairs / House Robber — space-optimizedO(n)
The Idea

Recursion Basics ended on naive Fibonacci: fib(n) = fib(n-1) + fib(n-2), and it's O(2ⁿ) because it recomputes the exact same subproblem over and over — fib(5) calls fib(3) twice, fib(4) calls fib(2) twice, and it only gets worse the deeper you go. Dynamic programming is the fix for exactly that symptom, and nothing more: remember the answer to a smaller problem you've already solved, so you never solve it twice. That's the whole idea. Everything else in this phase is that one sentence applied to bigger and bigger problems.

There are two ways to apply it. Memoization is top-down: you keep the recursion exactly as it was, but before doing any work you check a cache — has this exact input been solved before? If yes, hand back the stored answer instantly. If no, solve it like normal, then write the answer into the cache before returning, so the next time (yours or someone else's) it's free. Tabulation is bottom-up: no recursion at all. You build an array of answers starting from the smallest subproblem — the base case — and fill it in order, each new entry built from entries you already computed. Same idea, opposite direction: memoization starts at the big problem and works down to base cases as needed; tabulation starts at the base cases and works up to the big problem, always.

Think of it like a sticky note. The first time you work out some sub-calculation by hand, you jot the answer on a sticky note. Next time you need that exact same sub-calculation, you don't redo the arithmetic — you glance at the note. Memoization is sticking the note on the fridge the moment you first compute something, in case you need it again. Tabulation is writing out every note you're going to need, in order, before you start using them.

DP is not a new kind of algorithm — it's naive recursion with a cache. If you can write the recursive version and name its overlapping subproblem, adding memoization is often a two-line change: check the cache first, write to the cache last.
Build It
Memoization vs. tabulation, side by side
# top-down (memoization): keep the recursion, add a cache
function solve(n, memo = {}):
    if n in memo:                // seen this exact input before — don't redo the work
        return memo[n]
    if n is a base case:
        return direct answer      // base cases don't need caching, they're already O(1)
    answer = combine(solve(smaller n, memo), ...)
    memo[n] = answer              // write the sticky note before returning
    return answer

# bottom-up (tabulation): no recursion — fill an array from the base case up
function solve(n):
    dp = array of size n + 1
    dp[0] = base case answer      // seed the base case(s) BY HAND first
    dp[1] = base case answer
    for i from 2 to n:
        dp[i] = combine(dp[i - 1], dp[i - 2], ...)   // build on answers already filled in
    return dp[n]
Fibonacci, naive — O(2ⁿ), the problem restated
function fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)   // recomputes fib(3), fib(2)... many times over
This is the exact function from Recursion Basics. Nothing below changes what it computes — only how many times it redoes work it's already done.
Fibonacci, memoized — top-down, O(n)
function fib(n, memo = {}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
    return memo[n]
Same recursive shape as naive fib, plus a cache check at the top and a cache write at the bottom. Every fib(k) is now computed exactly once, ever — that's what turns O(2ⁿ) into O(n).
Fibonacci, tabulated — bottom-up, O(n) time, O(1) space
function fib(n):
    if n <= 1:
        return n
    prev2 = 0    // fib(0)
    prev1 = 1    // fib(1)
    for i from 2 to n:
        curr = prev1 + prev2
        prev2 = prev1
        prev1 = curr
    return prev1
No array at all — fib(i) only ever needs the previous two values, so two variables replace an n-length table. This is the first real space-optimization moment in the phase: you don't always need to keep the whole table around, only however much of it the recurrence actually looks back at.
Climbing Stairs — dp[i] = number of ways to reach step i
function climbStairs(n):
    if n <= 2:
        return n
    dp = array of size n + 1
    dp[1] = 1   // one way to reach step 1: a single 1-step
    dp[2] = 2   // two ways to reach step 2: 1+1, or one 2-step
    for i from 3 to n:
        dp[i] = dp[i - 1] + dp[i - 2]   // last move was a 1-step, or last move was a 2-step
    return dp[n]
The recurrence is identical to Fibonacci's — this is the same relabeling Recursion Basics already pointed out. What's new here is the discipline: dp[i] is defined in plain English first ("number of distinct ways to reach step i") before a single line of the recurrence is written.
House Robber — dp[i] = best haul using houses 0..i
function rob(nums):
    if len(nums) == 1:
        return nums[0]
    dp = array of size len(nums)
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    for i from 2 to len(nums) - 1:
        dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])   // skip house i, or rob it (and skip i-1)
    return dp[len(nums) - 1]
dp[i] means "the most money robbable from the first i+1 houses, obeying the no-two-adjacent rule." At each house you have exactly two options — leave it (carry forward dp[i-1]) or take it (dp[i-2] plus this house's cash) — and dp[i] is whichever is bigger. Stating that sentence out loud before coding is the actual skill this topic is teaching, more than the recurrence itself.
Know It

The headline comparison in this topic is dp[n] in O(n) versus the naive recursive version it replaces in O(2ⁿ) — a jump from exponential to linear, for the cost of a cache. Space for a full 1D table is O(n); when the recurrence only looks back a fixed number of steps (Fibonacci, Climbing Stairs — both only need i-1 and i-2), that drops to O(1) with a couple of rolling variables instead of an array.

OperationTimeSpaceWhy
Fibonacci — naive recursiveO(2ⁿ)O(n)Calls roughly double each level down (Recursion Basics); the O(n) is stack depth, not extra data structures — no cache exists yet.
Fibonacci — memoized (top-down)O(n)O(n)Every fib(k) for k from 0 to n is computed exactly once and then read from cache forever after — n distinct subproblems, O(1) work each beyond the recursive calls themselves.
Fibonacci — tabulated, rolling variablesO(n)O(1)Same n subproblems, filled in a loop instead of recursion — and since the recurrence only ever needs the last two answers, only two variables need to exist at once.
Climbing Stairs / House Robber — full tableO(n)O(n)One dp[i] computed per index, each in O(1) using already-filled entries.
Climbing Stairs / House Robber — space-optimizedO(n)O(1)Both recurrences only reach back to i-1 and i-2 — the rolling-variable trick from Fibonacci applies unchanged.
Break It

Forgetting to seed the base case(s) by hand

the recurrence loop runs before dp[0] / dp[1] are set

dp[i] = dp[i-1] + dp[i-2] is meaningless until dp[0] and dp[1] (or whatever the smallest indices are) hold real values — the recurrence has nothing correct to build on otherwise. This is the tabulation twin of Recursion Basics' "missing base case": in recursion a missing base case never terminates, in tabulation it just quietly produces garbage or crashes on an out-of-bounds read, which is often harder to notice.

Inconsistent state definition

dp[i] shifts meaning partway through the function

Does dp[i] mean "using the first i items" or "the answer at item i itself"? Both are legitimate choices, but the recurrence, the base cases, and the final return value all have to agree on which one — mixing them mid-function (using an i-1-indexed base case against an i-indexed recurrence, for instance) is one of the most common DP bugs there is. Say the definition out loud in a full sentence before writing any code, the way Build It does for House Robber, and check every line against that sentence.

A memo cache shared across separate calls

the same cache object is reused for a different input

A memo dictionary keyed only by n (not by n and whatever else the problem depends on) will happily return a cached answer computed for a completely different array or target — correct-looking output, wrong problem. The usual fix is passing a fresh cache per top-level call, or keying it by every value the recurrence actually depends on, not just the loop index.

Space-optimizing before the full version is verified correct

jumping straight to rolling variables instead of a full dp[] array first

O(1)-space Fibonacci is three lines different from O(n)-space Fibonacci, but debugging which of those three lines is wrong is much harder without an array you can print and inspect. Get the full-table version correct first — it's easier to reason about and easier to test — then collapse it to rolling variables once you trust it. Optimizing space is the last step, not the first.

Use It
Climbing Stairs
Revisit from Recursion Basics — this time, name the DP state explicitly: dp[i] = number of ways to reach step i, dp[i] = dp[i-1] + dp[i-2].
Easy
Fibonacci Number
Revisit from Recursion Basics — replace the naive O(2ⁿ) call with the memoized or tabulated version from Build It.
Easy
House Robber
The rob() function from Build It, verbatim: dp[i] = max(skip house i, rob house i).
Medium
House Robber II
Houses are now in a circle, so house 0 and the last house can't both be robbed — run House Robber's dp twice, once excluding house 0 and once excluding the last house, take the max.
Medium
Min Cost Climbing Stairs
Same recurrence shape as Climbing Stairs, but take a min of costs instead of a sum of counts: dp[i] = cost[i] + min(dp[i-1], dp[i-2]).
Easy
N-th Tribonacci Number
Fibonacci's recurrence with a third term: dp[i] = dp[i-1] + dp[i-2] + dp[i-3] — three rolling variables instead of two.
Easy
Delete and Earn
Bucket the values by number and sum their totals, then this is House Robber wearing a disguise: taking value v forbids v-1 and v+1, the same adjacency rule as robbing a house.
Medium
Maximum Subarray
Revisit from Sliding Window, now framed as DP (Kadane's algorithm): dp[i] = max(nums[i], dp[i-1] + nums[i]) — either start fresh at i, or extend the best subarray ending at i-1.
Medium
Domino and Tromino Tiling
A wider recurrence than Fibonacci's — work out how many ways a 2×n board can be tiled given the ways to tile 2×(n-1), 2×(n-2), and the partial-tromino states, and let dp track all of them.
Medium
Decode Ways
dp[i] = number of ways to decode the first i characters; add dp[i-1] if the single digit at i-1 is valid (1-9), add dp[i-2] if the two-digit number at i-2..i-1 is valid (10-26).
Medium