Skip to Content
Course content

408: Dynamic Programming Patterns in Python

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Dynamic Programming (DP) usually sounds a lot more intimidating than it actually is. When I first encountered it, I thought it was some high-level mathematical wizardry. In reality, DP is just a fancy way of saying "don't calculate the same thing twice." If you've already solved a sub-problem, just remember the answer and look it up later.

To see this in action, let's build a solver for a common problem: finding the minimum cost to travel across a grid. Imagine a 2D grid where each cell has a cost (like a toll). You start at the top-left and can only move right or down. Your goal is to reach the bottom-right spending as little as possible.

Tackling the Grid with Simple Recursion

My instinct is always to start with the most intuitive path. If I'm at cell (r, c), the cost to get to the end is the cost of the current cell plus the minimum of the costs from the cell to my right or the cell below me. Here is how I'd write that in Python:

def min_cost(grid, r, c):
    # Base case: we reached the bottom-right corner
    if r == len(grid) - 1 and c == len(grid[0]) - 1:
        return grid[r][c]
    
    # Out of bounds check
    if r >= len(grid) or c >= len(grid[0]):
        return float('inf')
    
    # The recursive step
    return grid[r][c] + min(min_cost(grid, r + 1, c), 
                            min_cost(grid, r, c + 1))

This looks clean, right? It’s elegant and follows the logic of the problem perfectly. But there's a massive problem here that I've fallen into plenty of times during interviews.

Hitting the Exponential Wall

I tried running the code above with a 3x3 grid, and it worked instantly. Then I tried a 15x15 grid, and my laptop started sounding like a jet engine. Why? Because this function is recalculating the same cells thousands of times. To calculate cell (1,1), it asks for (1,2) and (2,1). But (1,2) also asks for (2,2), and (2,1) also asks for (2,2). We are solving the same sub-problems over and over again.

This is the "overlapping sub-problems" characteristic that tells us we need DP. The time complexity here is exponential, which is a death sentence for any real-world application.

Caching the Path with Memoization

The quickest fix in Python is "Top-Down DP," also known as memoization. We keep the recursive structure but add a dictionary to store results. Now, before calculating a cell, we check if we've already done it. You could use a dictionary, but Python's functools.lru_cache is a professional's shortcut for this.

from functools import lru_cache

def solve_grid(grid):
    rows, cols = len(grid), len(grid[0])

    @lru_cache(None)
    def min_cost(r, c):
        if r == rows - 1 and c == cols - 1:
            return grid[r][c]
        if r >= rows or c >= cols:
            return float('inf')
        
        return grid[r][c] + min(min_cost(r + 1, c), min_cost(r, c + 1))

    return min_cost(0, 0)

Suddenly, that 15x15 grid finishes in milliseconds. We've traded a bit of memory (the cache) for a massive gain in speed. This is the "Top-Down" approach: we start at the goal and work backward to the base cases.

Switching to Tabulation for Efficiency

While memoization is great, recursion has a limit (the stack depth). If you have a grid with 2,000 rows, you'll hit a RecursionError. To avoid this, we use "Bottom-Up DP," or tabulation. Instead of recursion, we use a table (usually a 2D list) and fill it iteratively.

I like to think of this as building a foundation. We solve the smallest possible problem first (the destination cell) and use those answers to build up to the starting cell.

def min_cost_tabulated(grid):
    rows, cols = len(grid), len(grid[0])
    # Create a DP table of the same size
    dp = [[0] * cols for _ in range(rows)]
    
    # Initialize the bottom-right corner
    dp[rows-1][cols-1] = grid[rows-1][cols-1]
    
    # Fill the last row (can only come from the right)
    for c in range(cols - 2, -1, -1):
        dp[rows-1][c] = grid[rows-1][c] + dp[rows-1][c+1]
        
    # Fill the last column (can only come from below)
    for r in range(rows - 2, -1, -1):
        dp[r][cols-1] = grid[r][cols-1] + dp[r+1][cols-1]
        
    # Fill the rest of the grid
    for r in range(rows - 2, -1, -1):
        for c in range(cols - 2, -1, -1):
            dp[r][c] = grid[r][c] + min(dp[r+1][c], dp[r][c+1])
            
    return dp[0][0]

Now we have no recursion limits, and we've completely eliminated the overhead of function calls. This is the gold standard for DP: identify the state, define the transition, and fill the table.




πŸ“‹ Practical Task

Exercise: Implementing a Coin Change Optimizer

You are building a cash register system. Given a list of coin denominations (e.g., [1, 5, 10, 25]) and a target amount, you need to find the minimum number of coins required to make that amount. If the amount cannot be reached, return -1.

Instead of using recursion, implement a Bottom-Up Tabulation approach. Create a DP list where dp[i] represents the minimum coins needed to make the amount i. Initialize the list with a value larger than the amount (like float('inf')) and set dp[0] = 0.

Requirements:

  • Function signature: def min_coins_needed(coins, amount):
  • Use a single-dimensional DP table.
  • Iterate through every amount from 1 up to the target amount.
  • For each amount, iterate through the available coin denominations to find the minimum.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.