학습 목표: DP 개념 & 점화식 도출 · Top-Down vs Bottom-Up · 1차원 DP 대표 유형 · 피보나치, 계단 오르기, 동전 교환 등
1️⃣ DP란 무엇인가?
개념
큰 문제를 작은 부분 문제로 나누고, 이미 계산한 결과를 저장해서 재사용하는 기법.
🧾 비유: 수학 시험에서 한 번 푼 계산 중간값을 지우지 않고 풀이 옆에 적어두는 것!
같은 계산을 또 할 때 다시 풀지 않고 옆에 적어둔 값을 그대로 쓴다.
DP가 적용 가능한 조건 2가지
-
최적 부분 구조 (Optimal Substructure)
→ 전체 문제의 최적해가 부분 문제의 최적해로 구성됨
→ 예: 최단 경로의 부분 경로도 최단 경로
-
중복 부분 문제 (Overlapping Subproblems)
→ 같은 부분 문제가 여러 번 반복해서 등장함
→ 예: fib(5) 계산할 때 fib(3)이 여러 번 등장
이 두 조건이 모두 성립하면 → DP 적용 가능!
그리디는 매 순간 최선, DP는 모든 경우를 저장하며 최선 을 찾음.
DP vs 재귀 vs 그리디
- 재귀: 같은 계산을 중복해서 함 → 느림
- 재귀 + 메모이제이션 : 결과를 저장 → Top-Down DP
- 반복문 + 테이블 : 작은 것부터 채워나감 → Bottom-Up DP
- 그리디 : 저장 없이 매 순간 최선 선택 → 빠르지만 항상 최적 아님
2️⃣ Top-Down vs Bottom-Up
Top-Down (메모이제이션, Memoization)
재귀로 구현하되 이미 계산한 결과는 딕셔너리/배열에 저장. 필요할 때만 계산하는 방식 (Lazy Evaluation).
# 피보나치 - Top-Down
def fib_topdown(n, memo={}):
if n <= 1:
return n
if n in memo: # 이미 계산했으면 바로 반환
return memo[n]
memo[n] = fib_topdown(n-1, memo) + fib_topdown(n-2, memo)
return memo[n]
print(fib_topdown(10)) # 55
✅ 장점: 필요한 부분만 계산, 재귀적 사고가 직관적
❌ 단점: 재귀 깊이 한계, 함수 호출 오버헤드
Bottom-Up (타뷸레이션, Tabulation)
작은 문제부터 순서대로 테이블을 채워나가는 방식. 반복문으로 구현, 재귀 없음.
# 피보나치 - Bottom-Up
def fib_bottomup(n):
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]
print(fib_bottomup(10)) # 55
✅ 장점: 재귀 없어서 스택 오버플로우 걱정 없음, 속도 빠름
❌ 단점: 필요 없는 부분도 계산할 수 있음
언제 뭘 쓰냐
코테에서 일반적으로 → Bottom-Up 추천
점화식 관계가 복잡할 때 → Top-Down이 직관적
n이 크거나 재귀 깊이 우려 시 → Bottom-Up
3️⃣ 점화식 도출 방법
DP의 핵심은 점화식(Recurrence Relation) 세우기야.
점화식 도출 3단계
1. dp[i]의 의미를 명확하게 정의한다
→ "dp[i] = i번째 계단까지 올라가는 최대 점수"
2. 마지막 선택(결정)이 무엇인지 생각한다
→ "i번째 계단에 도달하려면 i-1 또는 i-2에서 왔을 것"
3. 그 선택들 중 최적을 고른다
→ dp[i] = max(dp[i-1], dp[i-2]) + score[i]
핵심 사고법:"dp[i]를 구하려면 dp[i-?]에서 어떻게 왔냐?" 를 생각하기
4️⃣ 1차원 DP 대표 유형
유형 1 - 피보나치 수열
# dp[i] = i번째 피보나치 수
# dp[i] = dp[i-1] + dp[i-2]
def fibonacci(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]
print(fibonacci(10)) # 55
유형 2 - 1로 만들기
정수 N에 3가지 연산을 최소한으로 사용해서 1을 만들어라
- 3으로 나누어 떨어지면 3으로 나누기
- 2로 나누어 떨어지면 2로 나누기
- 1 빼기
# dp[i] = i를 1로 만드는 최소 연산 횟수
# dp[i] = min(dp[i//3], dp[i//2], dp[i-1]) + 1
def make_one(n):
dp = [0] * (n + 1)
for i in range(2, n + 1):
dp[i] = dp[i-1] + 1 # 무조건 -1 가능
if i % 3 == 0:
dp[i] = min(dp[i], dp[i//3] + 1)
if i % 2 == 0:
dp[i] = min(dp[i], dp[i//2] + 1)
return dp[n]
print(make_one(10)) # 3 (10 → 9 → 3 → 1)
점화식 도출 과정:
dp[1] = 0 (이미 1이니까) dp[2] = 1 (2 ÷ 2 = 1, 1번) dp[3] = 1 (3 ÷ 3 = 1, 1번) dp[4] = 2 (4 ÷ 2 = 2 ÷ 2 = 1, 2번) dp[10] = 3 (10 → 9 → 3 → 1)
유형 3 - 계단 오르기
계단마다 점수가 있고, 규칙이 있을 때 최대 점수를 구해라
- 한 번에 1칸 또는 2칸만 오를 수 있다
- 연속된 3개의 계단을 밟으면 안 된다
- 마지막 계단은 반드시 밟아야 한다
# dp[i] = i번째 계단까지 올라갈 때 최대 점수
# 경우 1: i-1 계단에서 1칸 올라옴 → 연속 2칸이면 i-3에서 왔어야 함
# dp[i] = dp[i-3] + score[i-1] + score[i]
# 경우 2: i-2 계단에서 2칸 올라옴 (i-1 안 밟음)
# dp[i] = dp[i-2] + score[i]
def stair(scores):
n = len(scores)
if n == 1:
return scores[0]
if n == 2:
return scores[0] + scores[1]
dp = [0] * n
dp[0] = scores[0]
dp[1] = scores[0] + scores[1]
dp[2] = max(scores[0] + scores[2], scores[1] + scores[2])
for i in range(3, n):
# 경우 1: i-1도 밟고 i도 밟음 (i-2는 못 밟음)
case1 = dp[i-3] + scores[i-1] + scores[i]
# 경우 2: i-1은 밟지 않고 i만 밟음
case2 = dp[i-2] + scores[i]
dp[i] = max(case1, case2)
return dp[n-1]
scores = [10, 20, 15, 25, 10, 20]
print(stair(scores)) # 75 (10+25+20+20)
핵심: 마지막 계단(i)을 밟는 방법은 딱 2가지!
방법 1: i-2 → i (i-1은 건너뜀) 방법 2: i-3 → i-1 → i (연속 2칸만) → i-2 → i-1 → i (연속 3칸) 은 금지!
유형 4 - 동전 교환 (최소 동전 수)
n원을 만드는 데 필요한 동전의 최소 개수는?
# dp[i] = i원을 만드는 최소 동전 수
# dp[i] = min(dp[i - coin] + 1) for coin in coins if i >= coin
def min_coins(n, coins):
dp = [float('inf')] * (n + 1)
dp[0] = 0 # 0원은 동전 0개
for i in range(1, n + 1):
for coin in coins:
if i >= coin and dp[i - coin] != float('inf'):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[n] if dp[n] != float('inf') else -1
coins = [1, 5, 10, 25]
print(min_coins(36, coins)) # 3 (25+10+1)
print(min_coins(11, coins)) # 2 (10+1)
점화식 도출 과정:
dp[0] = 0 dp[1] = dp[0] + 1 = 1 (1짜리 동전) dp[5] = dp[0] + 1 = 1 (5짜리 동전) dp[6] = dp[5] + 1 = 2 (5짜리 + 1짜리) dp[11] = dp[10] + 1 = 2 (10짜리 + 1짜리)
유형 5 - 동전 교환 (경우의 수)
n원을 만드는 방법의 수는? (동전 종류 순서 무관)
# dp[i] = i원을 만드는 방법의 수
def count_coins(n, coins):
dp = [0] * (n + 1)
dp[0] = 1 # 0원 만드는 방법: 아무것도 안 쓰는 1가지
for coin in coins:
for i in range(coin, n + 1):
dp[i] += dp[i - coin]
return dp[n]
coins = [1, 2, 5]
print(count_coins(5, coins)) # 4
# (1+1+1+1+1), (1+1+1+2), (1+2+2), (5)
⚠️ 최소 동전 수 vs 경우의 수 차이:
최소 동전 수: coin 루프 안에 i 루프 → dp[i] = min(...) 경우의 수: coin 루프 안에 i 루프 → dp[i] += dp[i-coin] 루프 순서는 같지만 점화식이 다른거!!!
5️⃣ 점화식 도출 과정 발표 예시
DP 문제 풀이 과정을 말로 설명할 때의 구조임
예시 - 계단 오르기 발표
- dp 정의:
- dp[i] = i번째 계단까지 도달했을 때 얻을 수 있는 최대 점수
- 기저 조건: dp[0] = score[0] dp[1] = score[0] + score[1] dp[2] = max(score[0]+score[2], score[1]+score[2])
- 점화식:
i번째 계단을 밟는 경우는 2가지
- i-1도 밟은 경우: i-2는 못 밟으므로 dp[i-3] + score[i-1] + score[i]
- i-1을 안 밟은 경우: dp[i-2] + score[i] → dp[i] = max(dp[i-3] + score[i-1] + score[i], dp[i-2] + score[i])
- 정답: dp[n-1]
핵심 요약
✅ DP 적용 조건
→ 최적 부분 구조 + 중복 부분 문제 둘 다 성립
✅ Top-Down vs Bottom-Up
→ Top-Down: 재귀 + 메모이제이션, 직관적
→ Bottom-Up: 반복문 + 테이블, 빠르고 안정적
→ 코테 기본: Bottom-Up 추천
✅ 점화식 도출 순서
→ dp[i]의 의미 정의 → 마지막 선택 분석 → 점화식 작성
✅ 1차원 DP 대표 유형
→ 피보나치: dp[i] = dp[i-1] + dp[i-2]
→ 1로 만들기: dp[i] = min(dp[i//3], dp[i//2], dp[i-1]) + 1
→ 계단: dp[i] = max(dp[i-3]+s[i-1]+s[i], dp[i-2]+s[i])
→ 동전 최소: dp[i] = min(dp[i-coin]+1)
→ 동전 경우수: dp[i] += dp[i-coin]
DP - 언제 뭘 쓸지 정리
- 최솟값/최댓값을 구해라 + 이전 선택이 영향을 줌 → DP (그리디가 아니라 DP인 이유: 모든 경우 고려 필요)
- 경우의 수를 구해라+ 부분 문제가 반복됨 → DP (재귀로 짜면 시간초과 → 메모이제이션/테이블)
- 연속 조건이 있는 최대/최소 → dp[i] 정의 시 마지막 선택을 잘 분류해야 함
- 점화식이 안 떠오를 때 → dp[1], dp[2], dp[3] 손으로 직접 채워보기 → 패턴이 보이면 일반화
프로그래머스 추천 문제 5개
프로그래머스 피보나치 수 https://school.programmers.co.kr/learn/courses/30/lessons/12945
프로그래머스 N으로 표현 https://school.programmers.co.kr/learn/courses/30/lessons/42895
프로그래머스 정수 삼각형 https://school.programmers.co.kr/learn/courses/30/lessons/43105
프로그래머스 등굣길 https://school.programmers.co.kr/learn/courses/30/lessons/42898
프로그래머스 도둑질 https://school.programmers.co.kr/learn/courses/30/lessons/42897
문제풀이
문제 - 프로그래머스 피보나치 수
https://school.programmers.co.kr/learn/courses/30/lessons/12945
문제 설명
피보나치 수는 F(0) = 0, F(1) = 1일 때, 1 이상의 n에 대하여 F(n) = F(n-1) + F(n-2) 가 적용되는 수 입니다.
예를들어
- F(2) = F(0) + F(1) = 0 + 1 = 1
- F(3) = F(1) + F(2) = 1 + 1 = 2
- F(4) = F(2) + F(3) = 1 + 2 = 3
- F(5) = F(3) + F(4) = 2 + 3 = 5
와 같이 이어집니다.
2 이상의 n이 입력되었을 때, n번째 피보나치 수를 1234567으로 나눈 나머지를 리턴하는 함수, solution을 완성해 주세요.
제한 사항
- n은 2 이상 100,000 이하인 자연수입니다.
입출력 예
| n | return |
|---|---|
| 3 | 2 |
| 5 | 5 |
입출력 예 설명
피보나치수는 0번째부터 0, 1, 1, 2, 3, 5, ... 와 같이 이어집니다.
문제가 잘 안풀린다면😢
힌트가 필요한가요? [코딩테스트 연습 힌트 모음집]으로 오세요! → 클릭
