LeetCode Dynamic Programming for Beginners: A Complete Step-by-Step Tutorial

Michael Zhang
Oct 16, 2025
22 min read
Dynamic ProgrammingLeetCodeAlgorithmsInterview PrepMemoizationOptimization
Dynamic programming is one of the most challenging topics in coding interviews. This comprehensive guide breaks down DP concepts, patterns, and problem-solving strategies with clear examples to help beginners master this essential technique.

If you've been avoiding dynamic programming (DP) problems on LeetCode, you're not alone. DP has a reputation for being intimidating—problems seem to require magical insights, solutions look like they came from nowhere, and the "aha!" moment often feels out of reach.

But here's the truth: dynamic programming isn't magic. It's a systematic approach to solving problems by breaking them into smaller subproblems and storing results to avoid redundant calculations.

This guide will demystify DP from the ground up. By the end, you'll understand not just how to solve DP problems, but why the approach works and when to use it. We'll start with the fundamentals and build up to more complex patterns, with plenty of examples along the way.

What Is Dynamic Programming?

Dynamic programming is a problem-solving technique that solves complex problems by:

  1. Breaking them into smaller, overlapping subproblems
  2. Solving each subproblem only once
  3. Storing the results to avoid recalculating them
  4. Building up to the final solution using stored results

The key insight is that many problems have overlapping subproblems. Without DP, you might solve the same subproblem multiple times, leading to exponential time complexity. With DP, you solve each subproblem once and reuse the result, dramatically improving efficiency.

The Core Concept: Memoization vs. Tabulation

There are two main approaches to implementing DP:

1. Top-Down (Memoization): Start with the original problem and recursively break it down, storing results as you go. This is often more intuitive.

2. Bottom-Up (Tabulation): Start with the smallest subproblems and build up to the solution. This is usually more space-efficient.

Both approaches solve the same problem; the choice is often a matter of preference and problem structure.

When to Use Dynamic Programming

Not every problem needs DP. Here are the telltale signs that DP might be appropriate:

1. Overlapping Subproblems

If solving the problem requires solving the same smaller problem multiple times, DP can help.

Example: In the Fibonacci sequence, calculating fib(5) requires fib(3) and fib(4), and fib(4) requires fib(3) again. Without memoization, you'd calculate fib(3) twice.

2. Optimal Substructure

The optimal solution to the problem contains optimal solutions to its subproblems.

Example: In the "Coin Change" problem, the minimum coins needed for amount n is 1 plus the minimum coins needed for n - coin_value (for some coin). The optimal solution builds on optimal subproblem solutions.

3. Decision-Making Problems

Problems where you make a series of decisions and want to find the optimal sequence.

Example: "House Robber" — at each house, decide whether to rob it or skip it, maximizing total value.

4. Counting or Optimization Problems

Problems asking for:

  • Maximum or minimum values
  • Count of ways to do something
  • Whether something is possible

The DP Problem-Solving Framework

Here's a systematic approach that works for most DP problems:

Step 1: Identify the Problem Type

Ask yourself:

  • Are there overlapping subproblems?
  • Does the problem ask for optimization or counting?
  • Can I express the solution in terms of smaller versions of the same problem?

Step 2: Define the State

State is what you need to remember to solve a subproblem. It's usually expressed as:

  • A function: dp(i) = solution for subproblem ending at index i
  • A 2D array: dp[i][j] = solution for subproblem with parameters i and j

Key Question: What information do I need to solve this subproblem?

Step 3: Find the Recurrence Relation

The recurrence relation expresses how to compute dp[i] using previously computed values.

General Form: dp[i] = f(dp[i-1], dp[i-2], ..., dp[0])

Key Question: How can I express the current state in terms of previous states?

Step 4: Set Base Cases

Base cases are the smallest subproblems that can be solved directly without recursion.

Key Question: What are the simplest cases I can solve immediately?

Step 5: Determine the Order of Computation

For bottom-up DP, determine the order in which to fill the DP table.

Key Question: Which subproblems need to be solved before others?

Step 6: Implement and Optimize

Write the code, then look for space optimization opportunities (e.g., if you only need the last few values, you might not need the entire array).

Example 1: Fibonacci Numbers (The Classic Introduction)

Let's start with the simplest DP problem: computing the nth Fibonacci number.

Problem: Calculate the nth Fibonacci number, where fib(0) = 0, fib(1) = 1, and fib(n) = fib(n-1) + fib(n-2).

Naive Recursive Solution (Inefficient)

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

Time Complexity: O(2^n) — exponential!
Why? We recalculate the same values repeatedly. For example, fib(5) calls fib(3) twice, fib(2) three times, etc.

Top-Down DP (Memoization)

def 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]
python

Time Complexity: O(n) — each value calculated once
Space Complexity: O(n) — for the memo dictionary and call stack

Bottom-Up DP (Tabulation)

def fib(n):
    if n <= 1:
        return n
    
    dp = [0] * (n + 1)
    dp[0] = 0
    dp[1] = 1
    
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]
python

Time Complexity: O(n)
Space Complexity: O(n)

Space-Optimized Version

Since we only need the last two values:

def fib(n):
    if n <= 1:
        return n
    
    prev2 = 0  # fib(0)
    prev1 = 1  # fib(1)
    
    for i in range(2, n + 1):
        current = prev1 + prev2
        prev2 = prev1
        prev1 = current
    
    return prev1
python

Time Complexity: O(n)
Space Complexity: O(1) — only storing two variables!

Example 2: Climbing Stairs

This is a classic DP problem that's essentially Fibonacci in disguise.

Problem: You are climbing a staircase. It takes n steps to reach the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Example: For n = 3, there are 3 ways: (1,1,1), (1,2), (2,1)

Step-by-Step Solution

Step 1: Identify the Problem Type

  • Overlapping subproblems: Yes — to reach step n, we need ways to reach step n-1 and n-2
  • Optimal substructure: Yes — the solution builds on smaller solutions
  • Counting problem: Yes — we're counting distinct ways

Step 2: Define the State

  • dp[i] = number of distinct ways to reach step i

Step 3: Find the Recurrence Relation

  • To reach step i, you can come from step i-1 (1 step) or step i-2 (2 steps)
  • dp[i] = dp[i-1] + dp[i-2]

Step 4: Set Base Cases

  • dp[0] = 1 (one way to be at the start: don't move)
  • dp[1] = 1 (one way: take 1 step)

Step 5: Implementation

def climbStairs(n: int) -> int:
    if n <= 2:
        return n
    
    dp = [0] * (n + 1)
    dp[0] = 1
    dp[1] = 1
    
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]
python

Space-Optimized:

def climbStairs(n: int) -> int:
    if n <= 2:
        return n
    
    prev2 = 1
    prev1 = 1
    
    for i in range(2, n + 1):
        current = prev1 + prev2
        prev2 = prev1
        prev1 = current
    
    return prev1
python

Example 3: House Robber

This introduces the concept of making decisions at each step.

Problem: You are a robber planning to rob houses along a street. Each house has a certain amount of money. You cannot rob two adjacent houses. Determine the maximum amount of money you can rob.

Example:

  • Input: [2, 7, 9, 3, 1]
  • Output: 12 (rob houses at index 0 and 2: 2 + 9 = 11, or index 1 and 3: 7 + 3 = 10, or index 0, 2, 4: 2 + 9 + 1 = 12)

Solution Approach

Step 1: Identify the Problem Type

  • Decision-making: At each house, decide to rob or skip
  • Optimization: Maximize total money
  • Optimal substructure: Maximum for n houses depends on maximum for n-1 and n-2 houses

Step 2: Define the State

  • dp[i] = maximum money that can be robbed from houses 0 to i

Step 3: Find the Recurrence Relation
At house i, we have two choices:

  1. Rob it: Then we can't rob house i-1, so we get nums[i] + dp[i-2]
  2. Skip it: Then we get dp[i-1]

We want the maximum:

dp[i] = max(dp[i-1], nums[i] + dp[i-2])
python

Step 4: Base Cases

  • dp[0] = nums[0] (only one house)
  • dp[1] = max(nums[0], nums[1]) (two houses: rob the richer one)

Step 5: Implementation

def rob(nums: List[int]) -> int:
    n = len(nums)
    if n == 0:
        return 0
    if n == 1:
        return nums[0]
    if n == 2:
        return max(nums[0], nums[1])
    
    dp = [0] * n
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    
    for i in range(2, n):
        dp[i] = max(dp[i-1], nums[i] + dp[i-2])
    
    return dp[n-1]
python

Space-Optimized:

def rob(nums: List[int]) -> int:
    n = len(nums)
    if n == 0:
        return 0
    if n == 1:
        return nums[0]
    
    prev2 = nums[0]
    prev1 = max(nums[0], nums[1])
    
    for i in range(2, n):
        current = max(prev1, nums[i] + prev2)
        prev2 = prev1
        prev1 = current
    
    return prev1
python

Common DP Patterns

Understanding these patterns will help you recognize and solve DP problems faster:

Pattern 1: 1D DP (Linear)

Problems where the state depends on a single dimension (usually an index).

Examples: Fibonacci, Climbing Stairs, House Robber, Coin Change

Template:

dp = [0] * (n + 1)
dp[0] = base_case

for i in range(1, n + 1):
    dp[i] = recurrence_relation(dp[i-1], dp[i-2], ...)

return dp[n]
python

Pattern 2: 2D DP (Grid)

Problems involving grids or two parameters.

Examples: Unique Paths, Minimum Path Sum, Longest Common Subsequence

Template:

dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize first row and column

for i in range(1, m + 1):
    for j in range(1, n + 1):
        dp[i][j] = recurrence_relation(dp[i-1][j], dp[i][j-1], ...)

return dp[m][n]
python

Pattern 3: Knapsack Problems

Problems involving selecting items with constraints (weight, value, etc.).

Examples: 0/1 Knapsack, Coin Change, Partition Equal Subset Sum

Template:

dp = [[0] * (capacity + 1) for _ in range(len(items) + 1)]

for i in range(1, len(items) + 1):
    for w in range(1, capacity + 1):
        if items[i-1].weight <= w:
            dp[i][w] = max(
                dp[i-1][w],  # Don't take item
                items[i-1].value + dp[i-1][w - items[i-1].weight]  # Take item
            )
        else:
            dp[i][w] = dp[i-1][w]

return dp[len(items)][capacity]
python

Pattern 4: Longest Increasing Subsequence (LIS)

Problems involving finding the longest subsequence with certain properties.

Examples: Longest Increasing Subsequence, Russian Doll Envelopes

Template:

dp = [1] * n  # Each element is a subsequence of length 1

for i in range(1, n):
    for j in range(i):
        if nums[j] < nums[i]:  # Or other condition
            dp[i] = max(dp[i], dp[j] + 1)

return max(dp)
python

Pattern 5: Interval DP

Problems involving intervals or ranges.

Examples: Matrix Chain Multiplication, Palindrome Partitioning

Template:

dp = [[0] * n for _ in range(n)]

for length in range(2, n + 1):
    for i in range(n - length + 1):
        j = i + length - 1
        for k in range(i, j):
            dp[i][j] = min_or_max(
                dp[i][j],
                dp[i][k] + dp[k+1][j] + cost(i, k, j)
            )
python

Example 4: Coin Change (Unbounded Knapsack)

This problem introduces the concept of unlimited choices.

Problem: You are given coins of different denominations and a total amount. Find the minimum number of coins needed to make that amount. You may use each coin unlimited times.

Example:

  • Coins: [1, 3, 4], Amount: 6
  • Output: 2 (use two coins of value 3)

Solution Approach

Step 1: Identify the Problem Type

  • Optimization: Minimize number of coins
  • Unlimited choices: Can use each coin multiple times
  • Optimal substructure: Minimum coins for amount n = 1 + minimum coins for n - coin_value

Step 2: Define the State

  • dp[i] = minimum number of coins needed to make amount i

Step 3: Recurrence Relation
For each amount i, try each coin:

dp[i] = min(dp[i], 1 + dp[i - coin]) for each coin if coin <= i
python

Step 4: Base Case

  • dp[0] = 0 (0 coins needed for amount 0)

Step 5: Implementation

def coinChange(coins: List[int], amount: int) -> int:
    # Initialize with a large value (infinity)
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    
    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i:
                dp[i] = min(dp[i], 1 + dp[i - coin])
    
    return dp[amount] if dp[amount] != float('inf') else -1
python

Key Insight: We iterate through all amounts from 1 to amount, and for each amount, we try every coin. This ensures we find the minimum coins needed.

Debugging DP Solutions

When your DP solution isn't working, check these common issues:

Issue 1: Incorrect Recurrence Relation

Symptom: Solution gives wrong answers for test cases.

Fix: Re-examine your logic. Draw out the decision tree or state transitions manually for a small example.

Issue 2: Wrong Base Cases

Symptom: Solution fails for edge cases (empty input, single element, etc.).

Fix: Explicitly handle all base cases before the main loop.

Issue 3: Index Out of Bounds

Symptom: Runtime errors when accessing dp[i-k] for negative indices.

Fix: Add bounds checking:

if i - k >= 0:
    dp[i] = max(dp[i], dp[i-k] + value)
python

Issue 4: Initialization Errors

Symptom: Getting 0 or unexpected values.

Fix: Initialize the DP array correctly. For minimization problems, use a large value. For maximization, use 0 or a small value.

Issue 5: Wrong Iteration Order

Symptom: Solution works for some inputs but not others.

Fix: Ensure you're computing dependencies in the correct order. In bottom-up DP, compute smaller subproblems before larger ones.

Practice Strategy

To master DP, follow this progression:

Phase 1: Fundamentals (Week 1)

  • Fibonacci variations
  • Climbing Stairs
  • House Robber
  • Min Cost Climbing Stairs

Phase 2: 1D DP Patterns (Week 2)

  • Coin Change
  • Word Break
  • Decode Ways
  • Perfect Squares

Phase 3: 2D DP (Week 3)

  • Unique Paths
  • Minimum Path Sum
  • Longest Common Subsequence
  • Edit Distance

Phase 4: Advanced Patterns (Week 4)

  • Longest Increasing Subsequence
  • 0/1 Knapsack
  • Partition Equal Subset Sum
  • Target Sum

Phase 5: Complex Problems (Ongoing)

  • Regular Expression Matching
  • Burst Balloons
  • Palindrome Partitioning
  • Scramble String

Tips for Interview Success

  1. Start with the recurrence relation — Don't jump to code. Explain the state and transition first.

  2. Draw it out — Use a whiteboard to show the DP table filling up. This demonstrates understanding.

  3. Mention optimization — After giving the O(n) space solution, mention that you could optimize to O(1) if only the last few values are needed.

  4. Handle edge cases — Always check for empty inputs, single elements, and boundary conditions.

  5. Test with examples — Walk through your solution with the given example to verify correctness.

Common Mistakes to Avoid

Mistake 1: Overcomplicating the State

Problem: Defining state with unnecessary dimensions.

Example: For "Climbing Stairs," you don't need dp[i][j] to track position and steps taken. dp[i] (position) is sufficient.

Fix: Start simple. Add dimensions only if necessary.

Mistake 2: Not Recognizing DP When You See It

Problem: Trying greedy or other approaches when DP is more appropriate.

Fix: If you see overlapping subproblems and optimal substructure, try DP first.

Mistake 3: Incorrect Base Case Initialization

Problem: Using dp[0] = 0 when it should be dp[0] = 1 (or vice versa).

Fix: Think carefully about what the base case represents. For counting problems, dp[0] is often 1 (one way to do nothing).

Mistake 4: Forgetting to Return the Correct Value

Problem: Returning dp[n] when you should return dp[n-1], or vice versa.

Fix: Be consistent with your indexing. Document whether indices are 0-based or 1-based.

Integration with Learning Resources

When learning DP, having access to step-by-step hinting system can help you understand the recurrence relation without immediately seeing the solution. The key is getting guidance on state definition and transitions rather than the complete code.

For a structured approach to mastering DP along with other patterns, consider following a DSA learning path that introduces DP problems in a logical sequence, building complexity gradually.

FAQ

Q: How do I know if a problem is a DP problem?

A: Look for overlapping subproblems (solving the same thing multiple times), optimal substructure (optimal solution contains optimal subproblem solutions), and optimization/counting questions. If you find yourself saying "if I knew the answer to a smaller version, I could solve this," it's likely DP.

Q: Should I use top-down (memoization) or bottom-up (tabulation)?

A: Top-down is often more intuitive and matches the problem structure. Bottom-up is usually more space-efficient and avoids stack overflow for large inputs. In interviews, either is fine, but mention that you could optimize space if needed.

Q: What's the difference between DP and divide-and-conquer?

A: Divide-and-conquer breaks problems into independent subproblems (no overlap). DP handles overlapping subproblems by storing results. Merge sort is divide-and-conquer; Fibonacci is DP.

Q: How do I find the recurrence relation?

A: Ask: "What decisions can I make at this step?" Then express the current state in terms of states you've already computed. For optimization, use min or max. For counting, use addition.

Q: Can every DP problem be space-optimized?

A: Not always, but many 1D DP problems can be optimized from O(n) to O(1) or O(k) if you only need the last k values. 2D DP problems can sometimes be optimized from O(m×n) to O(min(m,n)) if you only need the previous row.

Q: How do I handle DP problems with multiple constraints?

A: Add dimensions to your state. For example, if you have both a capacity constraint and a count constraint, use dp[i][capacity][count]. Start with the necessary dimensions and optimize later if possible.

Q: What if I can't figure out the recurrence relation?

A: Start with the base cases and work forward. Ask: "What's the simplest case?" Then: "If I know the answer for size n-1, how do I get the answer for size n?" Draw out examples manually.

Conclusion

Dynamic programming is a powerful technique that transforms exponential-time problems into polynomial-time solutions. The key to mastering DP is understanding the fundamental concepts:

  • State definition: What information do you need to remember?
  • Recurrence relation: How do you build solutions from subproblems?
  • Base cases: What are the simplest problems you can solve directly?
  • Computation order: In what order should you solve subproblems?

Start with simple 1D problems like Fibonacci and Climbing Stairs to build intuition. Progress to decision-making problems like House Robber. Then tackle 2D problems and knapsack variants. With consistent practice, you'll develop pattern recognition that makes DP problems feel systematic rather than mysterious.

Remember: every expert was once a beginner. DP problems that seem impossible today will feel routine after you've solved 20-30 of them. The systematic framework outlined here will guide you through that journey.

The most important skill isn't memorizing solutions—it's recognizing when DP applies and knowing how to derive the recurrence relation from first principles. With that foundation, you can tackle new DP problems confidently in interviews and beyond.

Ready to Level Up Your LeetCode Learning?

Apply these techniques with LeetCopilot's AI-powered hints, notes, and mock interviews. Transform your coding interview preparation today.

Related Articles