Dynamic Programming Tutorial
Table of Contents
Introduction to Dynamic Programming
Dynamic programming (DP) is a method for solving complex problems by breaking them down into simpler subproblems.
Key concepts:
- Optimal Substructure
- Overlapping Subproblems
- Memoization
- Bottom-up vs Top-down approaches
Setup
from typing import List, Dict, Optional from functools import lru_cache import time def timing_decorator(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end - start:.4f} seconds") return result return wrapper
Classic Example: Fibonacci
Recursive (Naive) Approach
@timing_decorator def fib_recursive(n: int) -> int: """Calculate nth Fibonacci number recursively.""" if n <= 1: return n return fib_recursive(n-1) + fib_recursive(n-2) # Test test_fib = [fib_recursive(n) for n in range(10)] print(f"First 10 Fibonacci numbers (recursive): {test_fib}")
Memoization (Top-down)
@timing_decorator def fib_memo(n: int, memo: Dict[int, int] = None) -> int: """Calculate nth Fibonacci number with memoization.""" if memo is None: memo = {} if n <= 1: return n if n not in memo: memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo) return memo[n] # Using Python's built-in memoization @timing_decorator @lru_cache(maxsize=None) def fib_lru(n: int) -> int: """Calculate nth Fibonacci number with lru_cache.""" if n <= 1: return n return fib_lru(n-1) + fib_lru(n-2) # Test test_memo = [fib_memo(n) for n in range(10)] test_lru = [fib_lru(n) for n in range(10)] print(f"First 10 Fibonacci numbers (memoized): {test_memo}") print(f"First 10 Fibonacci numbers (lru_cache): {test_lru}")
Bottom-up Approach
@timing_decorator def fib_bottom_up(n: int) -> int: """Calculate nth Fibonacci number iteratively.""" if n <= 1: return n a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b # Test test_bottom_up = [fib_bottom_up(n) for n in range(10)] print(f"First 10 Fibonacci numbers (bottom-up): {test_bottom_up}")
Grid Problems
Path Finding
@timing_decorator def count_paths(grid: List[List[int]]) -> int: """ Count paths from top-left to bottom-right in a grid. Can only move right or down. 0 represents blocked cell. """ if not grid or not grid[0]: return 0 rows, cols = len(grid), len(grid[0]) dp = [[0] * cols for _ in range(rows)] # Initialize first cell dp[0][0] = 1 if grid[0][0] else 0 # Initialize first row for j in range(1, cols): if grid[0][j]: dp[0][j] = dp[0][j-1] # Initialize first column for i in range(1, rows): if grid[i][0]: dp[i][0] = dp[i-1][0] # Fill rest of the grid for i in range(1, rows): for j in range(1, cols): if grid[i][j]: dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[-1][-1] # Test test_grid = [ [1, 1, 1], [1, 0, 1], [1, 1, 1] ] print(f"Paths in test grid: {count_paths(test_grid)}")
String Problems
Longest Common Subsequence
@timing_decorator def lcs(text1: str, text2: str) -> str: """Find longest common subsequence between two strings.""" m, n = len(text1), len(text2) dp = [[0] * (n + 1) for _ in range(m + 1)] # Build length table for i in range(1, m + 1): for j in range(1, n + 1): if text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) # Reconstruct the subsequence result = [] i, j = m, n while i > 0 and j > 0: if text1[i-1] == text2[j-1]: result.append(text1[i-1]) i -= 1 j -= 1 elif dp[i-1][j] > dp[i][j-1]: i -= 1 else: j -= 1 return ''.join(reversed(result)) # Test s1, s2 = "ABCDGH", "AEDFHR" print(f"LCS of {s1} and {s2}: {lcs(s1, s2)}")
Exercises
Here are some practice problems to work through:
- Coin Change Problem
- Knapsack Problem
- Edit Distance
- Maximum Subarray
- Longest Increasing Subsequence
Solution: Coin Change
@timing_decorator def coin_change(coins: List[int], amount: int) -> int: """ Find minimum number of coins needed to make amount. Returns -1 if amount cannot be made. """ dp = [float('inf')] * (amount + 1) dp[0] = 0 for coin in coins: for x in range(coin, amount + 1): dp[x] = min(dp[x], dp[x - coin] + 1) return dp[amount] if dp[amount] != float('inf') else -1 # Test test_coins = [1, 2, 5] test_amount = 11 print(f"Minimum coins needed for {test_amount}: {coin_change(test_coins, test_amount)}")
Running the Examples
To run all examples:
if __name__ == '__main__': print("\n=== Testing Fibonacci Implementations ===") n = 35 fib_recursive(n) fib_memo(n) fib_lru(n) fib_bottom_up(n) print("\n=== Testing Grid Pathfinding ===") test_grid = [[1,1,1],[1,0,1],[1,1,1]] count_paths(test_grid) print("\n=== Testing String Problems ===") lcs("ABCDGH", "AEDFHR") print("\n=== Testing Coin Change ===") coin_change([1,2,5], 11)
Resources
- GeeksForGeeks DP
- LeetCode DP Problems
- Introduction to Algorithms (CLRS) - Chapter 15
Notes
This tutorial provides a foundation in dynamic programming. Key takeaways:
- Always look for overlapping subproblems
- Try to formulate recurrence relations
- Consider space-time tradeoffs
- Practice with variations of these patterns