Skip to main content
Core Techniques8 min read

Dynamic Programming Basics

Cache the answers to overlapping subproblems instead of recomputing them.

Dynamic programming has a scary reputation, but the core idea fits in one sentence: if you keep solving the same small problem over and over, solve it once and write down the answer. That is it. Behind the intimidating name (DP for short) is organized note-taking.

DP is the difference between an algorithm that takes centuries and one that finishes in a millisecond. Literally, as you are about to see with Fibonacci. It is also among the most common hard-question topics in interviews, so building the intuition early pays off for a long time.

The problem: naive recursion repeats itself

The Fibonacci sequence starts 0, 1, and every number after is the sum of the previous two: 0, 1, 1, 2, 3, 5, 8, 13. The definition is recursive (fib(n) = fib(n-1) + fib(n-2)) so the direct translation into code is two recursive calls and a base case.

Now trace fib(5). It calls fib(4) and fib(3). But fib(4) also calls fib(3). Both of those call fib(2), which gets computed three separate times, and fib(1) five times. The call tree doubles roughly every level, so the running time is exponential, about O(2^n). fib(50) at that rate is over a trillion calls. Minutes to hours of compute for a number you could work out on paper.

The wasted work has a name: overlapping subproblems. The recursion keeps arriving at the same question. What is fib(3)?. Through different paths, and dumbly recomputes it every time. The answer never changes. This is the exact situation DP exists to fix.

Java
static long fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

Memoization: top-down DP

Memoization (no r. Think memo, as in a note) is the smallest possible fix: before computing, check your notes; after computing, write the result down. Keep an array or HashMap keyed by the input. Each call first asks, have I solved fib(n) before? If yes, return the stored answer in O(1). If no, compute it the normal recursive way, store it, then return it.

This is called top-down because you still start from the big problem (fib(50)) and recurse downward; the cache just short-circuits repeats. Every distinct subproblem is now computed exactly once (there are only n + 1 of them) so the running time collapses from O(2^n) to O(n). fib(50) drops from a trillion calls to about a hundred.

The beauty of memoization is how mechanical it is: take any correct recursive solution whose answers depend only on the arguments, add a cache lookup at the top and a cache store before the return, and you are done. The recursive structure, base cases, and reasoning all stay identical.

Java
static long[] memo = new long[51];

static long fib(int n) {
    if (n <= 1) return n;
    if (memo[n] != 0) return memo[n];
    memo[n] = fib(n - 1) + fib(n - 2);
    return memo[n];
}

Tabulation: bottom-up DP

Tabulation reaches the same destination from the opposite direction. Instead of starting at fib(n) and recursing down, start at the smallest answers and build a table upward: fill in fib(0) and fib(1) directly, then compute fib(2) from them, then fib(3), looping until the table reaches n. No recursion at all, just an array and a for loop.

Each table entry is computed once from entries already filled in, so time is O(n) again, but tabulation has two extra perks. First, no call stack, so no risk of StackOverflowError on large inputs. Second, it exposes space optimizations: to fill entry i you only ever read entries i-1 and i-2, so you can throw away the rest of the table and keep just two variables, cutting space from O(n) to O(1).

Top-down or bottom-up is mostly taste. Memoization is easier to write because it follows the recursion you already found; tabulation is usually a bit faster and safer on deep inputs. In interviews, a common path is: derive the recursive relation, memoize it, then convert to a table if time permits.

Java
static long fib(int n) {
    if (n <= 1) return n;
    long prev = 0, curr = 1;
    for (int i = 2; i <= n; i++) {
        long next = prev + curr;
        prev = curr;
        curr = next;
    }
    return curr;
}

How to spot a DP problem

Two properties must hold. Overlapping subproblems: the naive recursion revisits the same states many times (unlike, say, merge sort, whose subproblems never repeat, that is divide and conquer, not DP). Optimal substructure: the best answer to the big problem can be assembled from best answers to smaller ones. Fibonacci trivially has both; so do most counting and optimization problems on sequences.

The problem statements give off recognizable signals. Phrases like count the number of ways, minimum cost, maximum profit, or longest subsequence (combined with choices at each step) usually mean DP. Classic first problems: Climbing Stairs (count ways to reach step n, which is Fibonacci in disguise), House Robber (max loot when adjacent houses cannot both be robbed), and Coin Change (fewest coins to make an amount).

A practical recipe for any of them: define the state in words (dp[i] is the best answer for the first i items), find the recurrence (how dp[i] follows from earlier entries), set base cases, and choose memoization or tabulation. The recurrence is the creative step and the rest is mechanical. DP tells you the best value achievable, and its natural rival is the greedy strategy, which tries to skip the table entirely by committing to the best-looking choice at each step. When that shortcut is safe, and when it quietly fails, is the next lesson.

key takeaways
DP means caching answers to subproblems that would otherwise be recomputed many times.
Naive recursive Fibonacci is O(2^n) purely because of repeated work; caching makes it O(n).
Memoization is top-down: keep the recursion and add a cache check and store.
Tabulation is bottom-up: fill a table from the base cases with a loop, avoiding recursion entirely.
Suspect DP when you see count the ways, minimum, maximum, or longest, plus choices whose subproblems overlap.

Frequently asked

What is the difference between memoization and tabulation?

Both cache subproblem answers; they differ in direction. Memoization keeps the recursive top-down structure and stores results as calls return, computing only the subproblems actually needed. Tabulation drops recursion and fills a table bottom-up from base cases with a loop, which avoids stack overflow and often enables reducing memory to a few variables.

Is dynamic programming related to divide and conquer?

They are cousins: both split a problem into smaller subproblems. The difference is that divide and conquer subproblems are disjoint and never repeat, like the two halves in merge sort, so there is nothing to cache. DP applies when subproblems overlap and the same state is reached repeatedly.

Why is it called dynamic programming?

The name is historical, coined by Richard Bellman in the 1950s, and programming there meant planning with tables, not writing code. He reportedly picked dynamic partly because it sounded impressive to funders. Do not look for meaning in the name; the technique is simply recursion plus caching.