Browse curriculum

Intro to Dynamic Programming

Understand the core concepts of DP: Overlapping Subproblems and Optimal Substructure.

Medium
10 min

Examples

Input:n = 5
Output:5
Explain:

The memoized run makes 9 calls: fib(2) and fib(3) are each asked for twice, and the second ask returns from the memo.

Input:n = 2
Output:1
Explain:

fib(1) and fib(0) are base cases, so nothing is reused yet — one value is stored and returned.

What is Dynamic Programming?

Dynamic Programming (DP) is a technique to solve complex problems by breaking them down into simpler subproblems. It stores the results of subproblems to avoid re-computing them.

Key Components:

  1. Overlapping Subproblems: The problem can be broken down into subproblems which are reused several times.
  2. Optimal Substructure: The optimal solution of the main problem can be constructed from optimal solutions of its subproblems.

The Motivation (Fibonacci)

Consider calculating the N-th Fibonacci number: F(n) = F(n-1) + F(n-2).

Usually, we might write a simple recursive function. But let's see why that is inefficient.

Attempt 1: Naive Recursion

The obvious recursion, straight from the definition:

def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

It is correct, and it is unusable past about n = 40. The next section shows why.

Visualizing the Inefficiency

Notice how fib(3) is computed from scratch every time it is asked for. The call tree has 2·fib(n+1) − 1 nodes, so the work grows exponentially — O(2ⁿ).

The Intuition: Cache It!

Why calculate fib(3) twice? Let's just remember the answer the first time we compute it.

This "remembering" is called Memoization (Top-Down DP).

Interactive Visualization

EVERY NODE IS A CALL PLAIN RECURSION MAKES · AMBER = ANSWERED FROM THE MEMO · THE GREY BELOW IT IS WHAT THE CACHE SAVED

LOOK IN THE MEMO

calls made

1

subtrees skipped

0

Answer

—

memo

Nothing cached yet

call stack

fib(5)

 

Enter fib(5). Not a base case, so look for 5 in the memo

Next

5 is not in the memo, so compute it once: fib(4) + fib(3)

n

n

5

Legend & Complexity

The call running now

Entered and returned

Answered from the memo — not recursed into

Never entered: the work the memo saved

Time

O(n)

Space

O(n)

Without the memo

O(2ⁿ)

Every node below is a call plain recursion would make. The memoized run lights the ones it actually enters — and an amber node is one that came back from the memo, so nothing underneath it is ever walked. That dim region is the work the cache removed.

Try n = 7: 13 calls against 41.

Dry Run (Memoization)

Calculating fib(5) with memo:

Call Cache Status Action Result
fib(5) Empty Call fib(4) Wait
fib(4) Empty Call fib(3) Wait
fib(3) Empty Call fib(2) Wait
fib(2) Empty Call fib(1), fib(0) Returns 1
... ... ... ...
fib(3) Found fib(2)=1 Reuse fib(2) Returns 2

Edge Cases & Common Mistakes

Watch out for these pitfalls:

  • Base cases first. solve has to answer k <= 1 before it looks in the memo, or the recursion has nothing to stop it.

  • One cache key per state. The key has to name the subproblem completely. In a two-dimensional DP that means both indices, not just one.

  • Deep recursion still overflows. Memoization removes repeated work, not depth. A run of 10⁵ nested calls will exhaust the stack in Python or Java, where the bottom-up form will not.

Solution Template

Top-down memoization — the method the figure above runs. The lines light up as it steps.

Intro to Dynamic Programming Code
1var fib = function(n) {
2 const memo = new Map();
3
4 const solve = (k) => {
5 if (k <= 1) {
6 return k;
7 }
8 if (memo.has(k)) {
9 return memo.get(k);
10 }
11 const value = solve(k - 1) + solve(k - 2);
12 memo.set(k, value);
13 return value;
14 };
15
16 return solve(n);
17};

When to use DP?

Use DP when:

  1. Optimization: Finding min/max/shortest/longest.
  2. Counting: Finding number of ways.
  3. Feasibility: Is it possible to reach X?

The approach, step by step

  1. Write the recursion first

    State the answer in terms of smaller versions of itself, and write the base cases that stop it.

  2. Add a cache on the way out

    Before recursing, return the stored answer if this input has been solved. After computing it, store it.

  3. Check what the cache removed

    Every distinct input is now computed once. Count the calls before and after to see the exponential tree collapse.

Frequently asked questions

What makes a problem a dynamic programming problem?

Two things together. Overlapping subproblems: a plain recursion would solve the same subproblem many times. Optimal substructure: the answer to the whole can be assembled from answers to those subproblems. Fibonacci has both, which is why caching turns it from exponential into linear.


What is the difference between memoization and tabulation?

Memoization is top-down — you keep the natural recursion and cache each result the first time you compute it. Tabulation is bottom-up — you fill an array from the base cases forward and never recurse. They do the same work in the same time; memoization is usually easier to write, tabulation avoids the call stack.


Why is naive Fibonacci recursion so slow?

fib(n) calls fib(n-1) and fib(n-2), and both of those recompute the same smaller values from scratch. The call tree has 2·fib(n+1) − 1 nodes, which grows exponentially. Memoization computes each distinct n exactly once, so 2n − 1 calls are enough.

Want to see the mechanism first? Step through it in the visualizer:

Dynamic Programming

Applying for software engineering roles? Free, no sign-up:

AI resume checker — match your resume to the job description