Dynamic Programming (DP) is an algorithm design technique that breaks complex problems into smaller subproblems and
stores (memoizes) already computed results to avoid redundant calculations.
Two conditions where DP can be applied:
Overlapping Subproblems: the same subproblem occurs repeatedly
Optimal Substructure: the overall optimal solution is composed of optimal solutions to subproblems
Understand the difference between memoization (top-down) and tabulation (bottom-up) and be able to implement each.
Systematically apply the three steps of a DP solution (define states → recurrence relation → initial values).
Solve basic DP problems like climbing stairs and coin change by defining states and recurrence relations on your own.
Key Concepts
3 Steps of DP Design
1. State Definition:
dp[i] = "Considering up to the i-th ..."
dp[i][j] = "Considering up to the i-th and j-th ..."
2. Recurrence Relation:
dp[i] = f(dp[i-1], dp[i-2], ...)
3. Base Case:
dp[0] = ..., dp[1] = ...
Memoization vs Tabulation
┌──────────────────┬────────────────────────┬──────────────────────────────┐
│ Feature │ Memoization (Top-down) │ Tabulation (Bottom-up) │
├──────────────────┼────────────────────────┼──────────────────────────────┤
│ Direction │ Big problem → Small problem │ Small problem → Big problem │
│ Implementation │ Recursion + Cache │ Loop + Table │
│ Required Computation │ Only what is actually needed │ Compute all subproblems │
│ Space │ O(n) + Recursion stack │ O(n) (no stack) │
│ Speed │ Function call overhead │ Usually faster │
│ Implementation Difficulty │ Intuitive │ Order needs consideration │
└──────────────────┴────────────────────────┴──────────────────────────────┘
dp[i] = The number of ways to climb to the i-th stair (1 step or 2 steps at a time)
dp[1] = 1 (1-step: {1})
dp[2] = 2 (2-steps: {1,1}, {2})
dp[i] = dp[i-1] + dp[i-2] (either came 1 step or 2 steps at the end)
→ Same recurrence relation as Fibonacci!
Coin Change (Reason DP is Needed)
Coins: [1, 5, 6, 9], Change: 11
Greedy: 9 + 1 + 1 = 3 coins
DP: 5 + 6 = 2 coins ← Optimal!
Reason greedy fails: local optimum ≠ global optimum
DP recurrence formula:
dp[i] = minimum number of coins needed to make i won
dp[0] = 0
dp[i] = min(dp[i - coin] + 1) for coin in coins if coin ≤ i
Example
File
Contents
src/01_fibonacci_memo.py
Fibonacci: Simple/Memoization/part@lru_cache
src/02_fibonacci_tab.py
Fibonacci: Tabulation, Space O(1)
src/03_climbing_stairs.py
Stair Climbing, Variant Problems
src/04_coin_change.py
Minimum Number of Coins, Number of Ways
Common Mistakes
Undefined State: You cannot set up a recurrence relation without first clearly defining what dp[i] means.
Missing Initial Values (Base Case): If you omit or incorrectly set dp[0], dp[1], all calculations will be wrong.
Incorrect Recurrence Direction: In bottom-up, when calculating dp[i], ensure that the required dp[j] (j < i) has already been computed.
Array Index Out of Range: When handling i - coin < 0 in dp[i - coin], omissions can occur.
Confusing with Greedy: DP is needed when "always choosing the largest" is not optimal — always check examples where greedy fails.
Top-down (recursion + cache) vs Bottom-up (iteration + table)
3-step design: define state → recurrence relation → initial values
Space optimization: If only the previous two are needed, can reduce to O(1)
Try It Yourself
Get the nth Fibonacci number using matrix exponentiation in O(log n)
Longest Increasing Subsequence (LIS) — prepare for the next lesson
Grid DP: number of paths from top-left to bottom-right in an m×n grid
Example files
The runnable files used in this lesson. Copy them into your own project and experiment.
src/01_fibonacci_memo.pyPython
#Topic: Fibonacci — Simple Recursion, Memoization, @lru_cache
#Time: O(2^n) simple recursion, O(n) memoization
#Space: O(n) common
import sys
import time
from functools import lru_cache
sys.setrecursionlimit(10_000)
#── Method 1: Simple Recursion (Inefficient) ────────────────────────────────────────────
def fib_naive(n: int) -> int:
"""Simple recursive Fibonacci.
Time: O(2^n) — Exponential redundant calculations
Space: O(n) — Recursion stack"""
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
#── Method 2: Manual Memoization ────────────────────────────────────────────
def fib_memo(n: int, cache: dict[int, int] | None = None) -> int:
"""Memoized Fibonacci.
Time: O(n) — Each n is calculated only once
Space: O(n) — Cache dictionary"""
if cache is None:
cache = {}
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
return cache[n]
#── Method 3: @lru_cache Decorator ─────────────────────────────────────────
@lru_cache(maxsize=None)
def fib_lru(n: int) -> int:
"""Automatic memoization using @lru_cache.
Time: O(n)
Space: O(n)"""
if n <= 1:
return n
return fib_lru(n - 1) + fib_lru(n - 2)
#── Method 4: Explicit Memo Table (Class Variable) ─────────────────────────────
_memo: dict[int, int] = {}
def fib_global_memo(n: int) -> int:
"""Global dictionary memoization.
Time: O(n) — Across all calls
Space: O(n)"""
if n in _memo:
return _memo[n]
if n <= 1:
_memo[n] = n
return n
_memo[n] = fib_global_memo(n - 1) + fib_global_memo(n - 2)
return _memo[n]
#── Performance Comparison ─────────────────────────────────────────────────────────
def benchmark(func, n: int, label: str) -> None:
start = time.perf_counter()
result = func(n)
elapsed = time.perf_counter() - start
print(f"{label:30s}: fib({n}) = {result}, Time = {elapsed:.6f}s")
if __name__ == "__main__":
print("=== Fibonacci Result Check ===")
for i in range(11):
print(f" fib({i:2d}) = {fib_lru(i)}", end="")
if i < 10:
print(",", end="")
print()
print("=== Performance Comparison ===")
n = 30
benchmark(fib_naive, n, "Simple Recursion")
benchmark(lambda x: fib_memo(x), n, "Manual Memoization")
benchmark(fib_lru, n, "@lru_cache")
benchmark(fib_global_memo, n, "Global Dictionary")
print("=== Limits of Simple Recursion ===")
print(f"fib(35) simple recursion: {fib_naive(35)} (slow...)")
print(f"fib(35) lru_cache: {fib_lru(35)} (instant)")
print("=== @lru_cache Cache Information ===")
info = fib_lru.cache_info()
print(f" hits={info.hits}, misses={info.misses}, size={info.currsize}")
src/02_fibonacci_tab.pyPython
#Topic: Fibonacci Tabulation (Bottom-up) and Space Optimization
#Time: O(n) Space: O(n) → O(1) Optimization
def fib_tabulation(n: int) -> int:
"""Tabulation Fibonacci (Bottom-up).
Fill the table in order from the smallest problem.
Time: O(n)
Space: O(n) — dp array"""
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
def fib_space_optimized(n: int) -> int:
"""Space-optimized Fibonacci.
dp[i] only needs dp[i-1] and dp[i-2] → 2 variables are enough
Time: O(n)
Space: O(1) ← key!"""
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
def fib_generator(count: int):
"""Fibonacci sequence generator.
Time: O(1) per yield
Space: O(1)"""
a, b = 0, 1
for _ in range(count):
yield a
a, b = b, a + b
def fib_matrix(n: int) -> int:
"""Fibonacci using matrix exponentiation O(log n).
[[1,1],[1,0]]^n = [[fib(n+1), fib(n)], [fib(n), fib(n-1)]]
Time: O(log n)
Space: O(log n) — recursion stack"""
if n <= 1:
return n
def mat_mul(a: list[list[int]], b: list[list[int]]) -> list[list[int]]:
return [
[a[0][0]*b[0][0] + a[0][1]*b[1][0],
a[0][0]*b[0][1] + a[0][1]*b[1][1]],
[a[1][0]*b[0][0] + a[1][1]*b[1][0],
a[1][0]*b[0][1] + a[1][1]*b[1][1]],
]
def mat_pow(m: list[list[int]], p: int) -> list[list[int]]:
if p == 1:
return m
if p % 2 == 0:
half = mat_pow(m, p // 2)
return mat_mul(half, half)
return mat_mul(m, mat_pow(m, p - 1))
base = [[1, 1], [1, 0]]
result = mat_pow(base, n)
return result[0][1]
#── Example Execution ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Tabulation vs Space Optimization ===")
print(f"{'n':>5} | {'Tabulation':>15} | {'Space O(1)':>15} | {'Matrix O(logn)':>15}")
print("-" * 58)
for n in [0, 1, 5, 10, 20, 30, 40]:
t = fib_tabulation(n)
s = fib_space_optimized(n)
m = fib_matrix(n)
print(f"{n:>5} | {t:>15} | {s:>15} | {m:>15}")
print("\n=== Tabulation Table Visualization (n=10) ===")
dp = [0] * 11
dp[1] = 1
print(f"dp[0] = {dp[0]}")
print(f"dp[1] = {dp[1]}")
for i in range(2, 11):
dp[i] = dp[i-1] + dp[i-2]
print(f"dp[{i}] = dp[{i-1}] + dp[{i-2}] = {dp[i-1]} + {dp[i-2]} = {dp[i]}")
print("\n=== Fibonacci Generator (First 15) ===")
fib_list = list(fib_generator(15))
print(f" {fib_list}")
print("\n=== Space Optimization Process Visualization ===")
prev2, prev1 = 0, 1
print(f"Initial: prev2={prev2}, prev1={prev1}")
for i in range(2, 11):
curr = prev1 + prev2
print(f" i={i}: {prev2} + {prev1} = {curr}")
prev2, prev1 = prev1, curr
print(f"Result: {prev1}")
src/03_climbing_stairs.pyPython
#Topic: Climbing Stairs DP
# Time: O(n) Space: O(n) → O(1)
def climb_basic(n: int) -> int:
"""Basic Stair Climbing: 1 or 2 steps at a time.
dp[i] = number of ways to reach step i
dp[i] = dp[i-1] + dp[i-2]
Time: O(n)
Space: O(n)"""
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1 # {1}
dp[2] = 2 # {1,1}, {2}
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
def climb_k_steps(n: int, k: int) -> int:
"""Variation: When you can climb up to k steps at a time.
dp[i] = dp[i-1] + dp[i-2] + ... + dp[i-k]
Time: O(n × k)
Space: O(n)"""
dp = [0] * (n + 1)
dp[0] = 1 #Step 0 = Starting point
for i in range(1, n + 1):
for step in range(1, k + 1):
if i - step >= 0:
dp[i] += dp[i - step]
return dp[n]
def climb_no_consecutive_two(n: int) -> int:
"""Baekjoon 2579 Variation: When you cannot climb 2 steps consecutively twice in a row.
State: dp[i][j]
i = current stair number
j = 0: this stair is the first in a consecutive sequence, j = 1: this stair is the second in a consecutive sequence
Actual problem 2579 does not allow 3 consecutive steps, so refer to the solution below:
dp[i][0] = stepping on stair i only once consecutively (jump 2 steps from i-2)
dp[i][1] = stepping on stair i twice consecutively (jump 1 step from i-1)
dp[i][0] = dp[i-2][0] + dp[i-2][1] (including score)
dp[i][1] = dp[i-1][0] (including score)
Time: O(n)
Space: O(n)"""
if n == 0:
return 0
if n == 1:
return 1
if n == 2:
return 2
#Simple stair count (score-less version)
dp0 = [0] * (n + 1) #First in a row
dp1 = [0] * (n + 1) #Second in a row
dp0[1] = 1
dp1[1] = 0
if n >= 2:
dp0[2] = 1
dp1[2] = 1
for i in range(3, n + 1):
dp0[i] = dp0[i - 2] + dp1[i - 2]
dp1[i] = dp0[i - 1]
return dp0[n] + dp1[n]
def climb_ways_space_optimized(n: int) -> int:
"""Space-optimized stair climbing.
Time: O(n)
Space: O(1)"""
if n <= 2:
return n
a, b = 1, 2
for _ in range(3, n + 1):
a, b = b, a + b
return b
#── Example Execution ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Basic Stair Climbing (1 step or 2 steps) ===")
print(f"{'n':>5} | {'Number of ways':>10}")
print("-" * 20)
for n in range(1, 11):
print(f"{n:>5} | {climb_basic(n):>10}")
print("=== Climbing up to k steps ===")
n = 10
for k in [1, 2, 3, 4]:
print(f"n={n}, k={k}: {climb_k_steps(n, k)} ways")
print("=== Case enumeration (n=4, 1 step or 2 steps) ===")
def enumerate_ways(n: int) -> list[list[int]]:
if n == 0:
return [[]]
if n < 0:
return []
ways = []
for step in [1, 2]:
for rest in enumerate_ways(n - step):
ways.append([step] + rest)
return ways
for n in range(1, 6):
ways = enumerate_ways(n)
print(f"n={n}: {len(ways)} ways → {ways[:3]}{'...' if len(ways) > 3 else ''}")
print("=== Space Optimization ===")
for n in [5, 10, 20, 50]:
print(f" climb({n}) = {climb_ways_space_optimized(n)}")
src/04_coin_change.pyPython
#Topic: Coin Change — Minimum Number (Greedy Failure), Number of Cases
# Time: O(amount × len(coins))
# Space: O(amount)
def min_coins(coins: list[int], amount: int) -> int:
"""Minimum Coin Count (DP).
dp[i] = minimum number of coins to make i won
dp[0] = 0
dp[i] = min(dp[i - coin] + 1) for coin ≤ i
Time: O(amount × len(coins))
Space: O(amount)"""
INF = float("inf")
dp = [INF] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i and dp[i - coin] + 1 < dp[i]:
dp[i] = dp[i - coin] + 1
return int(dp[amount]) if dp[amount] != INF else -1
def greedy_coins(coins: list[int], amount: int) -> int:
"""Greedy Coin Change (not always optimal!).
Time: O(len(coins) × log(len(coins))) — including sorting
Space: O(1)"""
coins_sorted = sorted(coins, reverse=True)
count = 0
remaining = amount
for coin in coins_sorted:
count += remaining // coin
remaining %= coin
return count if remaining == 0 else -1
def count_ways(coins: list[int], amount: int) -> int:
"""Number of ways to make amount with coins (order doesn't matter).
dp[i] = number of ways to make i won
Process each coin in order → avoids duplicate permutations
Time: O(amount × len(coins))
Space: O(amount)"""
dp = [0] * (amount + 1)
dp[0] = 1 #0 Won: 1 way without using coins
for coin in coins: #External: Coin types
for i in range(coin, amount + 1): #Internal: Amount
dp[i] += dp[i - coin]
return dp[amount]
def count_permutations(coins: list[int], amount: int) -> int:
"""Number of permutations to make amount with coins (order matters).
dp[i] = number of permutations to make i won
(outer: amount, inner: coin types)
Time: O(amount × len(coins))
Space: O(amount)"""
dp = [0] * (amount + 1)
dp[0] = 1
for i in range(1, amount + 1): #External: Amount
for coin in coins: #Internal: Coin types
if coin <= i:
dp[i] += dp[i - coin]
return dp[amount]
def reconstruct_coins(coins: list[int], amount: int) -> list[int] | None:
"""Recovering minimum coin combination.
Time: O(amount × len(coins))
Space: O(amount)"""
INF = float("inf")
dp = [INF] * (amount + 1)
dp[0] = 0
parent = [-1] * (amount + 1) #For Backtracking
for i in range(1, amount + 1):
for coin in coins:
if coin <= i and dp[i - coin] + 1 < dp[i]:
dp[i] = dp[i - coin] + 1
parent[i] = coin
if dp[amount] == INF:
return None
#Backtracking
result: list[int] = []
curr = amount
while curr > 0:
coin = parent[curr]
result.append(coin)
curr -= coin
return sorted(result, reverse=True)
#── Example Execution ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Greedy Failure Example ===")
coins_bad = [1, 5, 6, 9]
amount = 11
greedy_ans = greedy_coins(coins_bad, amount)
dp_ans = min_coins(coins_bad, amount)
print(f"Coins: {coins_bad}, Amount: {amount}")
print(f"Greedy: {greedy_ans} coins (9+1+1)")
print(f"DP: {dp_ans} coins (5+6)")
print("=== Korean Coins (Greedy Optimal) ===")
korean_coins = [500, 100, 50, 10, 5, 1]
for amount in [1260, 730, 999]:
g = greedy_coins(korean_coins, amount)
d = min_coins(korean_coins, amount)
print(f"{amount} Won: Greedy={g}, DP={d}, {'Match' if g == d else 'Mismatch!'}")
print("=== Number of Cases (Combination vs Permutation) ===")
coins = [1, 2, 3]
for amt in [3, 4, 5]:
comb = count_ways(coins, amt)
perm = count_permutations(coins, amt)
print(f"{amt} Won: Combination={comb}, Permutation={perm}")
print("=== Minimum Coin Combination Recovery ===")
test_cases = [(coins_bad, 11), ([1, 2, 5], 11), ([2], 3)]
for c, a in test_cases:
combo = reconstruct_coins(c, a)
print(f"Coins={c}, Amount={a}: {combo}")
Assignment
Problem 1: Baekjoon 2579 — Climbing Stairs (Silver III)
Given that each stair has a score, find the maximum score.
Rules: You cannot step on three consecutive stairs, and the last stair must be stepped on.
Recurrence Relation
dp[i][0] = The maximum when stepping on the i-th alone (jumped from i-2)
dp[i][1] = The maximum when stepping on the i-th consecutively (came from i-1)
dp[i][0] = max(dp[i-2][0], dp[i-2][1]) + score[i]
dp[i][1] = dp[i-1][0] + score[i]
Time/Space Complexity
Time: O(n), Space: O(n) → can be optimized to O(1)
Find the minimum number of operations to make an integer n into 1 using the following three operations:
If n is divisible by 3, divide by 3
If n is divisible by 2, divide by 2
Subtract 1
Recurrence Relation
dp[i] = the minimum number of operations to make i into 1
dp[1] = 0
dp[i] = dp[i-1] + 1
dp[i] = min(dp[i], dp[i//2] + 1) if i % 2 == 0
dp[i] = min(dp[i], dp[i//3] + 1) if i % 3 == 0
Time/Space Complexity
Time: O(n), Space: O(n)
Problem 3: Baekjoon 11726 — 2×n Tiling (Silver III)
Find the number of ways to fill a 2×n rectangle using 1×2 and 2×1 tiles, modulo 10007.
Recurrence Relation
dp[i] = Number of ways to fill a 2×i rectangle
dp[1] = 1 (1 vertical)
dp[2] = 2 (2 vertical or 2 horizontal)
dp[i] = dp[i-1] + dp[i-2] (mod 10007)
Time/Space Complexity
Time: O(n), Space: O(n)
Solution File
boj_2579.py
boj_1463.py
boj_11726.py
Show the reference solution
Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.
boj_11726.pyPython
#Baekjoon 11726 — 2×n Tiling
# https://www.acmicpc.net/problem/11726
# Time: O(n) Space: O(1)
import sys
input = sys.stdin.readline
MOD = 10_007
def solve() -> None:
n = int(input())
if n == 1:
print(1)
return
#dp[i] = Number of ways to tile 2×i
# dp[i] = dp[i-1] + dp[i-2]
#Space optimization: Use only 2 variables
a, b = 1, 2
for _ in range(3, n + 1):
a, b = b, (a + b) % MOD
print(b)
solve()
boj_1463.pyPython
#Baekjoon 1463 — Make to 1
# https://www.acmicpc.net/problem/1463
# Time: O(n) Space: O(n)
import sys
input = sys.stdin.readline
def solve() -> None:
n = int(input())
dp = [0] * (n + 1)
#dp[i] = Minimum number of operations to make i into 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + 1 #Operation 3: Subtract 1
if i % 2 == 0:
dp[i] = min(dp[i], dp[i // 2] + 1)
if i % 3 == 0:
dp[i] = min(dp[i], dp[i // 3] + 1)
print(dp[n])
solve()
boj_2579.pyPython
#Baekjoon 2579 — Climbing Stairs
# https://www.acmicpc.net/problem/2579
# Time: O(n) Space: O(n)
import sys
input = sys.stdin.readline
def solve() -> None:
n = int(input())
score = [0] + [int(input()) for _ in range(n)]
if n == 1:
print(score[1])
return
if n == 2:
print(score[1] + score[2])
return
#dp[i][0]: Maximum score when stepping on the i-th stair alone (jumping from i-2)
#dp[i][1]: Maximum score when stepping on the i-th stair consecutively (coming from i-1)
dp = [[0, 0] for _ in range(n + 1)]
dp[1][0] = score[1]
dp[1][1] = 0 #There is no case of stepping on the 1st stair consecutively (start)
dp[2][0] = score[2] #Skip the 1st, and step on the 2nd
dp[2][1] = score[1] + score[2] #Step on 1st → 2nd consecutively
for i in range(3, n + 1):
dp[i][0] = max(dp[i - 2][0], dp[i - 2][1]) + score[i]
dp[i][1] = dp[i - 1][0] + score[i]
print(max(dp[n][0], dp[n][1]))
solve()