f()

Recursion & DP

Divide and conquer. Memoize overlapping subproblems. Tabulate bottom-up.

Recursion & Dynamic Programming

Recursion breaks a problem into smaller sub-problems. DP = recursion + memoization (top-down) or tabulation (bottom-up) to avoid redundant work.

When to use DP: overlapping subproblems + optimal substructure.

Recursion Template

def solve(n):
    # 1. Base case(s) — must exist to terminate
    if n == 0:
        return 1
    # 2. Recursive case — smaller sub-problem
    return n * solve(n - 1)

Top-Down DP (Memoization)

Without memo, triple step is O(3ⁿ). With memo it's O(n).

# Option 1: lru_cache decorator (cleanest)
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

# Option 2: manual memo dict
def fib(n, memo={}):
    if n <= 1: return n
    if n in memo: return memo[n]
    memo[n] = fib(n-1, memo) + fib(n-2, memo)
    return memo[n]

Bottom-Up DP (Tabulation)

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

# Space-optimised: only need last 2 values
def fib_opt(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

Triple Step (Stair Climbing)

@lru_cache(maxsize=None)
def count_ways(n):
    if n < 0: return 0
    if n == 0: return 1       # one way to stand at bottom: do nothing
    return count_ways(n-1) + count_ways(n-2) + count_ways(n-3)

Coin Change (number of ways)

Key insight: process each coin in an outer loop to avoid counting permutations as distinct combinations.

def coin_change_ways(amount, coins):
    dp = [0] * (amount + 1)
    dp[0] = 1                      # one way to make 0: use no coins
    for coin in coins:
        for amt in range(coin, amount + 1):
            dp[amt] += dp[amt - coin]
    return dp[amount]

0/1 Knapsack

def knapsack(weights, values, capacity):
    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):
            dp[i][w] = dp[i-1][w]           # skip item i
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i][w],
                               dp[i-1][w - weights[i-1]] + values[i-1])
    return dp[n][capacity]

Powerset Generation (Backtracking)

Generate all 2ⁿ subsets:

def powerset(nums):
    result = [[]]
    for n in nums:
        result += [subset + [n] for subset in result]
    return result
# Time: O(2^n), Space: O(2^n)

Permutation Generation

def permutations(s):
    if len(s) == 0: return [""]
    result = []
    for i, char in enumerate(s):
        rest = s[:i] + s[i+1:]
        for perm in permutations(rest):
            result.append(char + perm)
    return result
# Time: O(n!), Space: O(n!)

Backtracking Template

Build solution incrementally, backtrack when a constraint is violated:

def backtrack(candidate, result, ...):
    if is_complete(candidate):
        result.append(candidate[:])   # save a copy
        return
    for next_choice in generate_choices(candidate):
        if is_valid(next_choice):
            candidate.append(next_choice)    # make move
            backtrack(candidate, result, ...)
            candidate.pop()                  # undo move (backtrack)

Common problems: N-Queens, Sudoku solver, permutations, combinations, word search.

Recursive Multiplication (no * operator)

def multiply(a, b):
    """Multiply using bit shifts and addition."""
    if b == 0: return 0
    if b == 1: return a
    half = multiply(a, b >> 1)   # b // 2 via right shift
    if b % 2 == 0:
        return half + half
    else:
        return half + half + a   # odd: add one more a
# x * 2 = x << 1 (left shift)

DP Pattern Summary

| Pattern | Example problems | |---------|-----------------| | 1D DP | Fibonacci, climbing stairs, house robber | | 2D DP | Longest common subsequence, edit distance, unique paths | | Knapsack | Subset sum, coin change, partition equal subsets | | Interval DP | Matrix chain multiplication, burst balloons |

Key Interview Tips

  • Always identify the recurrence relation before writing code
  • Draw the recursion tree — overlapping sub-trees = use DP
  • Top-down is easier to write; bottom-up is more space-efficient
  • State definition is everything in DP — what does dp[i] represent?
  • For backtracking: always undo your move after recursing