Skip to content

Dynamic Programming

Dynamic programming (DP) solves problems by breaking them into overlapping subproblems, solving each Subproblem once, and storing the results. Two properties must hold for DP to apply:

  1. Optimal substructure. The optimal solution to the problem contains optimal solutions to its subproblems
  2. Overlapping subproblems. The same subproblems are solved multiple times in a naive recursive solution

When both hold, DP reduces an exponential-time recursive solution to polynomial time.

Top-Down (Memoisation) vs Bottom-Up (Tabulation)

Section titled “Top-Down (Memoisation) vs Bottom-Up (Tabulation)”
AspectTop-Down (Memoisation)Bottom-Up (Tabulation)
ApproachRecursive with cachingIterative, fill table from base cases
OrderNatural recursion orderMust determine correct fill order
Stack spaceO(n)O(n) recursion depthO(1)O(1) (no recursion)
Cache controlOnly computes needed subproblemsComputes all subproblems
DebuggingEasier to reason aboutHarder to see the recurrence
PerformanceSlight overhead from recursionSlightly faster (no function call overhead)

How many distinct ways to climb nn stairs, taking 1 or 2 steps at a time?

dp[i]=dp[i1]+dp[i2]dp[i] = dp[i-1] + dp[i-2]

def climb_stairs(n):
"""
Number of ways to climb n stairs with 1 or 2 steps.
Time: O(n), Space: O(1) with rolling variables
"""
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev1
def climb_stairs_memo(n, memo=None):
"""
Top-down with memoisation. O(n) time, O(n) space (recursion + memo).
"""
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n <= 2:
return n
memo[n] = climb_stairs_memo(n - 1, memo) + climb_stairs_memo(n - 2, memo)
return memo[n]

Given an array of non-negative integers representing money at each house, maximise the amount you Can rob without robbing two adjacent houses.

dp[i]=max(dp[i1],dp[i2]+nums[i])dp[i] = \max(dp[i-1], dp[i-2] + nums[i])

def house_robber(nums):
"""
Maximum money from non-adjacent houses.
Time: O(n), Space: O(1)
"""
if not nums:
return 0
if len(nums) <= 2:
return max(nums)
prev2 = nums[0] # dp[i-2]
prev1 = max(nums[0], nums[1]) # dp[i-1]
for i in range(2, len(nums)):
current = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, current
return prev1

Given coins of different denominations and a target amount, find the minimum number of coins needed To make that amount. Return -1 if it is not possible.

dp[i] = \min(dp[i], dp[i - coin] + 1) \quad \mathrm{for each coin

def coin_change(coins, amount):
"""
Minimum coins to make amount.
Time: O(amount * len(coins)), Space: O(amount)
"""
# dp[i] = minimum coins to make amount i
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], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
def coin_change_combinations(coins, amount):
"""
Number of combinations to make amount (order doesn't matter).
Time: O(amount * len(coins)), Space: O(amount)
"""
dp = [0] * (amount + 1)
dp[0] = 1
# Process coins one at a time to avoid counting permutations
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[amount]
  • DP Patterns: Decision framework for identifying which DP pattern applies to a given problem.
  • Sorting Algorithms: Sorting techniques that can be combined with DP for optimisation problems.
  • Hashing and Hash Tables: Hash-based data structures used for memoisation in top-down DP implementations.