On This Page

What Is Dynamic Programming?

Dynamic Programming (DP) is a method for solving problems by breaking them into smaller subproblems โ€” smaller, self-similar versions of the original question. If those subproblems overlap (meaning a naive recursive approach would solve the same subproblem multiple times), DP saves each answer the first time and reuses it later. That's the core trick: compute once, use many times.

A Brief History

Richard Bellman invented dynamic programming in the 1950s while working at the RAND Corporation on optimization problems for the US Air Force. The name was deliberately vague. Bellman later explained that his boss hated the word "research" and mathematical terms made him uncomfortable, so Bellman chose "dynamic programming" because "dynamic" sounded energetic and hard to criticize, and "programming" referred to planning and scheduling (not computer programming). The name stuck, even though it tells you nothing about the technique.

Bellman also developed the Bellman equation, which is the foundation of modern reinforcement learning. Every time a self-driving car or game-playing AI makes a decision, it's solving a form of dynamic programming.

Two Requirements

Two things must be true for DP to apply:

DP vs Greedy vs Divide-and-Conquer

These three paradigms are often confused:

Where DP Shows Up

How It Works

There are two approaches, and they produce the same results. Pick whichever fits the problem better (or whichever makes more sense to you โ€” there's no wrong choice).

Top-Down (Memoization)

Write the recursive solution first, then add a cache. When a function is called with arguments you've seen before, return the stored result instead of recomputing. The word memoization (not "memorization") comes from "memo" โ€” you're leaving a note to yourself.

Bottom-Up (Tabulation)

Build a table starting from the smallest subproblems and work your way up. Each cell depends only on cells you've already filled in โ€” no recursion at all.

Choosing Top-Down vs Bottom-Up

Start top-down to get the recurrence right, then convert to bottom-up if you need space optimization or to avoid stack overflow. In interviews, top-down is faster to write. In production, bottom-up is more predictable. Both are correct.

Common DP Categories

Interactive Visualization

Watch how DP fills a table cell by cell. Switch between Fibonacci (1D) and Coin Change (tabulation) to see how dependencies flow. Hover over cells to see which earlier cells they depend on.

DP Table Builder

Common Mistakes

โš ๏ธ Mistake #1: Wrong base case For "number of ways" problems, dp[0] is usually 1 (there's exactly one way to make amount 0: use no coins). For "minimum cost" problems, dp[0] is usually 0 (zero cost to do nothing). Getting this wrong cascades through the entire table โ€” every cell depends on the base cases, so one wrong value corrupts everything.
โš ๏ธ Mistake #2: Wrong iteration order In bottom-up DP, you must fill cells in an order that ensures all dependencies are already computed. For standard 1D DP, iterate left to right. For knapsack (0/1), iterate the capacity right to left when using a 1D array (otherwise you'll use the same item twice). For LCS, fill row by row, left to right. If your results are wrong but the recurrence looks correct, check the fill order.
โš ๏ธ Mistake #3: Confusing 0/1 knapsack with unbounded knapsack In 0/1 knapsack, each item is used at most once. In unbounded (like coin change), items have unlimited copies. The recurrence is different: 0/1 looks at dp[i-1][w - weight] (previous row โ€” item not reused), unbounded looks at dp[i][w - weight] (current row โ€” item can be reused). Using the wrong one gives wrong answers.
โš ๏ธ Mistake #4: Not defining the state precisely "dp[i] = something about the first i elements" is too vague. Be exact: "dp[i] = the minimum number of coins needed to make amount i" or "dp[i][j] = the length of the longest common subsequence of text1[0..i-1] and text2[0..j-1]." If you can't precisely state what dp[i] represents, you can't write the transition correctly.
โš ๏ธ Mistake #5: Forgetting to initialize the DP table properly For "minimum" problems, initialize cells to infinity (or a very large number) so that any valid solution is better. For "maximum" problems, initialize to negative infinity (or 0). For "count" problems, initialize to 0 except the base case. Using Python's default of 0 for a "minimum" problem means every cell looks like it already has a perfect solution.

Complexity Analysis

DP doesn't have a single Big-O โ€” it depends on the problem. But there's a formula that works every time:

Time = (number of subproblems) ร— (work per subproblem)

Space = (number of subproblems stored)

For Fibonacci: n subproblems ร— O(1) work each = O(n). For LCS: mร—n subproblems ร— O(1) work each = O(mn). For LIS with binary search: n subproblems ร— O(log n) work each = O(n log n).

ProblemTimeSpaceOptimized Space
FibonacciO(n)O(n)O(1)
Climbing StairsO(n)O(n)O(1)
Coin ChangeO(n ร— m)O(n)โ€”
0/1 KnapsackO(n ร— W)O(n ร— W)O(W)
LCSO(m ร— n)O(m ร— n)O(min(m,n))
LISO(n log n)O(n)โ€”
Edit DistanceO(m ร— n)O(m ร— n)O(min(m,n))
Matrix Chain Mult.O(nยณ)O(nยฒ)โ€”
๐Ÿ’ก Space Optimization Trick Many 2D DP problems only look at the current row and the previous row. You can drop the full matrix and keep just two rows (or even one row with careful iteration order), cutting space from O(nยฒ) to O(n). For 1D problems like Fibonacci, you often only need 2-3 variables โ€” O(1) space. Always ask: "how far back do my dependencies go?"

Implementation

Fibonacci โ€” Top-Down vs Bottom-Up vs Space-Optimized

Python

# Top-down: recursive with memoization
def fib_memo(n, cache={}):
    if n <= 1:
        return n
    if n in cache:
        return cache[n]
    # Store result so we never recompute fib(n)
    cache[n] = fib_memo(n - 1) + fib_memo(n - 2)
    return cache[n]

# Bottom-up: iterative tabulation
def fib_tab(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        # Each cell only depends on the two before it
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

# Space-optimized: we only need 2 variables
# Since dp[i] depends only on dp[i-1] and dp[i-2],
# there's no reason to keep the entire array
def fib_opt(n):
    if n <= 1:
        return n
    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        prev2, prev1 = prev1, prev2 + prev1
    return prev1
      

Coin Change (Minimum Coins)

Python

def coin_change(coins, amount):
    """Find the fewest coins needed to make 'amount'.
    dp[i] = minimum coins to make amount i.
    Transition: dp[i] = min(dp[i - coin] + 1) for each coin.
    This is unbounded knapsack โ€” each coin can be used unlimited times."""
    
    # Initialize to infinity (no solution yet)
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0  # Base case: 0 coins to make amount 0

    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i and dp[i - coin] != float('inf'):
                dp[i] = min(dp[i], dp[i - coin] + 1)

    return dp[amount] if dp[amount] != float('inf') else -1
      

0/1 Knapsack

Python

def knapsack(weights, values, capacity):
    """Maximize total value without exceeding capacity.
    Each item can be taken at most once (0/1 = take or leave).
    dp[i][w] = max value using first i items with capacity w."""
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(capacity + 1):
            # Option 1: skip item i (don't take it)
            dp[i][w] = dp[i - 1][w]
            # Option 2: take item i (if it fits)
            if weights[i - 1] <= w:
                take = values[i - 1] + dp[i - 1][w - weights[i - 1]]
                dp[i][w] = max(dp[i][w], take)

    return dp[n][capacity]

# Space-optimized: 1D array, iterate capacity RIGHT to LEFT
# (to avoid using the same item twice in one row)
def knapsack_optimized(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for i in range(len(weights)):
        # MUST go right to left โ€” otherwise dp[w - weights[i]]
        # would use the current item's row, not the previous one
        for w in range(capacity, weights[i] - 1, -1):
            dp[w] = max(dp[w], values[i] + dp[w - weights[i]])
    return dp[capacity]
      

Longest Common Subsequence

Python

def lcs(text1, text2):
    """Find the length of the longest common subsequence.
    A subsequence is a sequence derived by deleting some (or no)
    elements without changing the order of remaining elements.
    Example: lcs("abcde", "ace") = 3 (the subsequence "ace")."""
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i - 1] == text2[j - 1]:
                # Characters match โ€” extend the LCS by 1
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                # No match โ€” take the better of skipping either character
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    return dp[m][n]
      

Edit Distance (Levenshtein Distance)

Python

def edit_distance(word1, word2):
    """Minimum operations (insert, delete, replace) to transform
    word1 into word2. Powers autocorrect, spell checkers, and
    DNA sequence alignment.
    dp[i][j] = edit distance between word1[0..i-1] and word2[0..j-1]."""
    m, n = len(word1), len(word2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    # Base cases: transforming to/from empty string
    for i in range(m + 1):
        dp[i][0] = i  # Delete all characters from word1
    for j in range(n + 1):
        dp[0][j] = j  # Insert all characters of word2

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]  # Characters match โ€” no cost
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j],      # Delete from word1
                    dp[i][j - 1],      # Insert into word1
                    dp[i - 1][j - 1]   # Replace in word1
                )

    return dp[m][n]
      

Longest Increasing Subsequence (O(n log n))

Python

import bisect

def lis(nums):
    """Find the length of the longest strictly increasing subsequence.
    The O(nยฒ) DP approach checks all previous elements for each position.
    This O(n log n) approach uses patience sorting (like sorting cards).
    
    tails[i] = smallest possible ending element of all increasing
    subsequences of length i+1 found so far."""
    tails = []
    for num in nums:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            # num is bigger than everything โ€” extend longest subsequence
            tails.append(num)
        else:
            # Replace to keep tails as small as possible
            # This doesn't change the LIS length but keeps options open
            # for future elements
            tails[pos] = num
    return len(tails)
      

Real-World Example: Autocorrect (Edit Distance)

When you type "recieve" and your phone suggests "receive," it's using edit distance. The algorithm computes the minimum number of character insertions, deletions, and replacements to transform one word into another, then suggests dictionary words with the smallest edit distance.

Python

def autocorrect(typed_word, dictionary, max_distance=2):
    """Suggest corrections for a misspelled word.
    Returns dictionary words within max_distance edits,
    sorted by edit distance (closest first).
    
    This is a simplified version of what your phone keyboard does.
    Real autocorrect also considers key proximity, word frequency,
    and context (what you typed before)."""

    def edit_dist(w1, w2):
        m, n = len(w1), len(w2)
        # Quick length check โ€” if lengths differ by more than
        # max_distance, we can skip the full DP computation
        if abs(m - n) > max_distance:
            return max_distance + 1

        dp = list(range(n + 1))  # Space-optimized: single row
        for i in range(1, m + 1):
            prev = dp[0]
            dp[0] = i
            for j in range(1, n + 1):
                temp = dp[j]
                if w1[i-1] == w2[j-1]:
                    dp[j] = prev
                else:
                    dp[j] = 1 + min(prev, dp[j], dp[j-1])
                prev = temp
        return dp[n]

    # Score every dictionary word
    suggestions = []
    for word in dictionary:
        dist = edit_dist(typed_word.lower(), word.lower())
        if dist <= max_distance:
            suggestions.append((dist, word))

    suggestions.sort()  # Sort by distance, then alphabetically
    return [word for dist, word in suggestions]

# Example
dictionary = ["receive", "recipe", "receipt", "recite", "deceive",
              "relieve", "believe", "retrieve", "achieve"]
print(autocorrect("recieve", dictionary))
# ['receive']  โ€” distance 1 (swap i and e)

print(autocorrect("receve", dictionary))
# ['receive', 'recite']  โ€” both distance 2
      

Edit distance is one of the most practically useful DP algorithms. Beyond autocorrect, it powers DNA sequence alignment (bioinformatics uses a weighted variant called the Needleman-Wunsch algorithm), plagiarism detection, and the Unix diff tool (which uses a variant of LCS).

Interview Patterns

๐Ÿ’ก How to Spot a DP Problem Look for these signals: "find the minimum/maximum," "count the number of ways," "is it possible to...," or "what's the longest/shortest." If the brute force involves trying all combinations and subproblems overlap, it's probably DP. If you can solve it greedily (locally optimal โ†’ globally optimal), it's not DP.

The DP Recipe

  1. Define the state: What does dp[i] (or dp[i][j]) represent? Write it in a comment. Be precise โ€” "dp[i] is the answer for i" is too vague.
  2. Find the transition: How does dp[i] relate to smaller subproblems? This is the recurrence relation. It's the hardest step.
  3. Set base cases: What's dp[0]? dp[1]? Handle the smallest inputs where the recurrence doesn't apply.
  4. Figure out the order: Which direction do you iterate? Make sure all dependencies are filled before you need them.
  5. Extract the answer: Is it dp[n]? max(dp)? dp[m][n]? Know where the final answer lives in your table.
๐Ÿ’ก 1D vs 2D โ€” When to Use Each If the state depends on a single index (position in array, remaining amount, current step), use 1D. If decisions involve two sequences (comparing two strings) or two dimensions (items ร— capacity), go 2D. When in doubt, start with whatever makes the recurrence clear, then optimize space.
๐Ÿ’ก "Take or Skip" Pattern Whenever you encounter an item and must decide whether to include it: dp[i] = max(take, skip). House robber: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) โ€” take this house (skip the previous) or skip this house. Knapsack: dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight] + value). This pattern covers a huge number of problems.
๐Ÿ’ก Converting Top-Down to Bottom-Up Start with the memoized recursive solution. The cache keys become your DP dimensions. The recursive calls become array lookups. The base cases of the recursion become the initial values in the table. The return value of the top-level call becomes the final cell you read.
๐Ÿ’ก Space Optimization Checklist Ask: "how far back do my dependencies go?" If dp[i] only depends on dp[i-1] and dp[i-2] โ†’ use two variables. If dp[i][j] only depends on dp[i-1][*] โ†’ use two rows (or one row with careful iteration). If dp[i][j] depends on dp[i-1][j-1], dp[i-1][j], and dp[i][j-1] โ†’ use one row plus a temp variable for the diagonal.

Recognizing DP Subcategories

Practice Problems

Sorted by difficulty. Start with the first two to build intuition, then tackle the harder ones.

ProblemDifficultyPatternLink
Climbing StairsEasy1D DP (Fibonacci variant)LC #70
House RobberMedium1D DP (take/skip)LC #198
Coin ChangeMediumUnbounded knapsackLC #322
Longest Increasing SubsequenceMedium1D DP / binary searchLC #300
Longest Common SubsequenceMedium2D DP (two strings)LC #1143
Partition Equal Subset SumMediumKnapsack / subset sumLC #416
Edit DistanceMedium2D DP (two strings)LC #72
Burst BalloonsHardInterval DPLC #312