Dynamic Programming and Recursion¶
Recursion fundamentals, memoization (top-down), bottom-up tabulation, and recognizing DP opportunities. Core technique for optimization problems with overlapping subproblems.
Key Facts¶
- Recursion needs three ingredients: base case, recursive call (moves toward base case), return value propagation
- Fibonacci without memoization: O(2^N); with memoization: O(N)
- Bottom-up (iterative) is usually more memory-efficient than top-down (memoized recursion)
- DP applies when there are overlapping subproblems - the recursive call tree has duplicate branches
- Memoization trades O(N) space for dramatic time savings
- Stack overflow occurs when recursion depth exceeds call stack memory
Patterns¶
Recursion Fundamentals¶
When to use recursion: problems with unknown or variable depth (file system traversal, tree operations, fractal structures).
# Directory traversal - unknown depth handled naturally
def find_directories(directory)
Dir.foreach(directory) do |filename|
if File.directory?("#{directory}/#{filename}") && filename != "." && filename != ".."
puts "#{directory}/#{filename}"
find_directories("#{directory}/#{filename}")
end
end
end
Writing recursive functions: identify single-step action, pass additional parameters to track state, last line is the recursive call.
def double_array(array, index=0):
if index >= len(array): return
array[index] *= 2
double_array(array, index + 1)
Memoization (Top-Down)¶
Cache results of expensive recursive calls:
def fib(n, memo={}):
if n <= 1: return n
if n not in memo:
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
Bottom-Up Tabulation¶
Iterate from small subproblems to large, store results in table:
def fib_bottom_up(n):
if n <= 1: return n
table = [0] * (n + 1)
table[1] = 1
for i in range(2, n + 1):
table[i] = table[i-1] + table[i-2]
return table[n]
DP Problem-Solving Framework¶
- Identify overlapping subproblems - same computation repeated with different parameters
- Define state: dp[i] = "best solution for problem of size i"
- Recurrence: dp[i] = f(dp[i-1], dp[i-2], ...)
- Base cases: smallest subproblems with known answers
- Build direction: bottom-up (iterative) or top-down (memoized)
Preprocessing Optimization¶
Example - find largest sub-square in matrix where all border cells are filled:
Naive: O(N^5). Better with preprocessing: O(N^3).
# Preprocess consecutive filled cells in each direction
zeros_right = [[0]*n for _ in range(n)]
zeros_below = [[0]*n for _ in range(n)]
for i in range(n-1, -1, -1):
for j in range(n-1, -1, -1):
if matrix[i][j] == 1:
zeros_right[i][j] = 1 + (zeros_right[i][j+1] if j+1 < n else 0)
zeros_below[i][j] = 1 + (zeros_below[i+1][j] if i+1 < n else 0)
Now checking a border candidate is O(1) per side instead of O(N).
Gotchas¶
- Python default mutable argument (
memo={}) persists across calls - useful for memoization but a common bug in other contexts - Recursion depth limit in Python: default ~1000 (
sys.setrecursionlimit()to change, but prefer iterative for deep recursion) - Bottom-up often allows space optimization (e.g., Fibonacci only needs prev two values, not full table)
- Not all recursive problems benefit from DP - only those with overlapping subproblems (not divide-and-conquer like mergesort)
See Also¶
- data structures fundamentals - recursion on arrays, Big O analysis
- trees and graphs - recursive tree traversal, DFS
- algorithm problem patterns - sliding window, two-pointer, interview techniques