동적 계획법 (Dynamic Programming - DP)
핵심 개념
DP 이전단계에서 구했던 최적의 값과, 현재상황에서 구한 값을 비교하여 더 최적의 값을 찾는 알고리즘이다.
동적 계획법의 특징
•
중복 부분 문제: 같은 문제를 반복적으로 해결
•
최적 부분 구조: 작은 문제의 해가 큰 문제의 해에 포함
•
메모이제이션: 한 번 계산한 결과를 저장해서 재사용
그래프 탐색 vs 동적 계획법
그래프 탐색 (BFS/DFS)
위 문제를 해결하기 위한 대표적인 알고리즘 세가지를 고려해 볼 수 있다.
1. 모든 경우의 수를 다 해볼것인가? 브루트포스(DFS,BFS)
Plain Text
복사
DP의 장점
2. 최적의 해를 향상 그리기에 찾는다. 그리디 알고리즘
과거 선택에대한 고려가 전혀 없어 미래를 선택하는 부분도 전혀 고려를 하지 않고 현재의
한때 선택한다.
3. DP 이전단계에서 구했던 최적의 값과, 현재상황에서 구한 값을 비교하여 더 최적의 값을 찾는 알고리즘이다.
Plain Text
복사
// factorial dp, 재귀함수 비교 예시
#include <iostream>
#include <vector>
int factorial(int n)
{
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int factorial_dp(int n)
{
if (n < 0)
return -1;
std::vector<int> dp(n + 1);
dp[0] = 1;
for (size_t i = 1; i < n + 1; i++)
dp[i] = dp[i - 1] * i;
return dp[n];
}
int main()
{
int fact = factorial(5);
fact = factorial_dp(5);
return 0;
}
C++
복사
동전 문제 (Coin Change Problem)
문제 상황
또 다른 예로 앞서 풀어봤던 문제죄에 거스름돈 문제가 있다.
화폐단위: 500원, 100원, 50원, 10원
손님이 받을 개수: 2개, 2개, 1개, 1개
1260원을 만드는 경우 이것 또한 그리디 알고리즘의 하나로 볼 수 있다.
Plain Text
복사
그리디 알고리즘의 한계
3,4,5원의 동전을 가지고 15원을 만들어보라고 가정해보자.
그리디로 해결하면:
•
5원 × 3개 = 15원 
•
하지만 다른 조합도 가능: 3원 × 5개 = 15원
DP 테이블로 문제 해결
기본 DP 문제 설정
위문제를 DP로 풀어보자
정확하고 대표적인 기본 DP 문제이다. dp 테이블을 만들어서 문제를 해결하면 된다.
만들어야 할 테이블의 크기를 정해준다. 금액보다 +1 한개 크게 만들어주면 된다.
3원 동전만 사용하는 경우
3원의 동전만 가지고 15원의 거스름을 가지려 좋지 않는 경우의수를 테이블을 만들어보자
| 금액 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13| 14| 15|
|------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3원 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
이번에는 3,4원의 동전을 가지고 만들 수 있는 경우의 수를 생각해보자
| 금액 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13| 14| 15|
|------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3원 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
| 3,4원| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 1 | 1 | 2 |
Plain Text
복사
금액 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
3 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
3,4 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 1 | 1 | 2 |
3,4,5 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 4 |
거스름돈이 4원이 만들어질때까지는 3원까지의 조합과 동일하다.
DP 알고리즘 단계별 분석
DP 점화식 도출
논리색이 현재 동전을 추가하기전 거슬러 줄 수 있는 경우의수 + 현재 가장 큰 동전을 하나 먼저 거슬러주고 남은 돈을 거슬러 줄 수 있는지에 대한 정답이다.
0+0 은 0 이기 때문에 (3,4) 조합으로 5원을 거슬러 줄 수 있다
dp[index] += dp[index - 현재 동전]
Plain Text
복사
4원 동전 추가 후 변화
4원 부터는 달라진다.
| 금액 | 0 | 1 | 2 | 3 | 4 |
|------|---|---|---|---|---|
| 3원 | 1 | 0 | 0 | 1 | 0 |
| 3,4원| 1 | 0 | 0 | 1 | 1 |
5원의 경우에는 3,4원으로 만들 수 있는 경우의수가 없다.
그렇다면 3,4원으로 5원을 거슬러 줄 경우에는 4원을 내주고 1원을 거슬러 줄 수 있는 경우의 조합이 있을 것이 불 수 있다.
| 금액 | 0 | 1 | 2 | 3 | 4 | 5 |
|------|---|---|---|---|---|---|
| 3원 | 1 | 0 | 0 | 1 | 0 | 0 |
| 3,4원| 1 | 0 | 0 | 1 | 1 | 0 |
|3,4,5 | 1 | 0 | 0 | 1 | 1 | 1 |
Plain Text
복사
DP 구현 코드
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main()
{
// 동전 집합과 목표 금액
vector<int> coins = {3, 4, 5};
int amount = 15;
// DP 테이블 초기화
vector<int> dp(amount + 1, 0);
dp[0] = 1; // 0원을 만드는 방법은 1가지 (아무것도 선택하지 않기)
// 각 동전에 대해
for (int coin : coins)
{
// 해당 동전부터 목표 금액까지
for (int i = coin; i <= amount; i++)
{
dp[i] += dp[i - coin]; // 점화식
}
}
// 결과 출력
if (dp[amount] == 0)
{
cout << "해당 금액을 만들 수 없습니다." << endl;
}
else
{
cout << amount << "원을 만들기 위한 최소 동전 개수: " << dp[amount] << endl;
}
return 0;
}
C++
복사
DP 실행 과정 시뮬레이션
3원 동전 처리
coin = 3일 때:
i=3: dp[3] += dp[0] = 0 + 1 = 1
i=6: dp[6] += dp[3] = 0 + 1 = 1
i=9: dp[9] += dp[6] = 0 + 1 = 1
...
Plain Text
복사
4원 동전 추가
coin = 4일 때:
i=4: dp[4] += dp[0] = 0 + 1 = 1
i=7: dp[7] += dp[3] = 0 + 1 = 1
i=8: dp[8] += dp[4] = 0 + 1 = 1
...
Plain Text
복사
5원 동전 추가
coin = 5일 때:
i=5: dp[5] += dp[0] = 0 + 1 = 1
i=8: dp[8] += dp[3] = 1 + 1 = 2 (3+5 또는 4+4)
i=10: dp[10] += dp[5] = 1 + 1 = 2
...
Plain Text
복사
DP 테이블 완성 과정
최종 DP 테이블
결과적으로 이것들은 패턴이 보이기 시작한다.
| 금액 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13| 14| 15|
|------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3원 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
| 3,4원| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 1 | 1 | 2 |
7원의 경우 (현재 동전) 추가되지전 거슬러 줄 수 있는 경우의수 0) + (4원 거슬러주고 남은 3원 거슬러 줄 수 있는 경우의수 1) = 1이 된다.
Plain Text
복사
DP의 핵심 원리
1. 상태 정의
dp[i] = i원을 만들 수 있는 경우의 수
C++
복사
2. 초기값 설정
dp[0] = 1; // 0원을 만드는 방법은 1가지
C++
복사
3. 점화식
dp[i] += dp[i - coin]; // 현재 동전을 사용해서 만드는 경우
C++
복사
4. 계산 순서
•
동전별로 순차 처리
•
각 동전에 대해 해당 동전 값부터 목표까지 계산
다양한 DP 문제 유형
1. 동전 교환 문제 변형들
•
최소 동전 개수: dp[i] = min(dp[i], dp[i-coin] + 1)
•
경우의 수: dp[i] += dp[i-coin]
•
가능 여부: dp[i] = dp[i] || dp[i-coin]
2. 배낭 문제 (Knapsack)
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i]);
C++
복사
3. 피보나치 수열
dp[i] = dp[i-1] + dp[i-2];
C++
복사
DP 설계 체크리스트
DP 적용 가능 조건
1.
최적 부분 구조: 작은 문제의 최적해가 큰 문제의 최적해에 포함
2.
중복 부분 문제: 같은 하위 문제가 여러 번 등장
3.
무후효성: 현재 상태가 미래에만 영향, 과거와 독립
DP 구현 단계
1.
상태 정의: dp[i]가 무엇을 의미하는지 명확히
2.
점화식 도출: 상태 간의 관계식 설정
3.
기저 조건: 초기값 설정
4.
계산 순서: Bottom-up 또는 Top-down
연습 문제
1.
계단 오르기: 1칸 또는 2칸씩 올라갈 수 있을 때 n번째 계단에 도달하는 방법의 수
2.
최장 증가 부분 수열: 배열에서 증가하는 부분 수열의 최대 길이
3.
편집 거리: 두 문자열을 같게 만들기 위한 최소 편집 횟수
4.
0-1 배낭 문제: 용량 제한이 있는 배낭에 최대 가치로 물건 담기
피보나치 수열 DP 분석
문제 정의
피보나치 수열: F(n) = F(n-1) + F(n-2), F(0) = 0, F(1) = 1
수열: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
F(10) = 55를 구해보자
Plain Text
복사
일반 재귀 vs DP 비교
일반 재귀 (비효율적)
int fibonacci_recursive(int n)
{
if (n <= 1) return n;
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2);
}
문제점: F(5)를 계산할 때
F(5)
/ \
F(4) F(3)
/ \ / \
F(3) F(2) F(2) F(1)
... (중복 계산 발생)
시간복잡도: O(2^n) - 매우 비효율적!
C++
복사
DP 해결 (효율적)
int fibonacci_dp(int n)
{
vector<int> dp(n + 1);
dp[0] = 0; // 기저조건
dp[1] = 1; // 기저조건
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i-1] + dp[i-2]; // 점화식
}
return dp[n];
}
시간복잡도: O(n) - 효율적!
C++
복사
DP 테이블 구성 과정
F(10)을 구하는 과정:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10|
|----|---|---|---|---|---|---|---|---|---|---|---|
|dp[i]| 0 | 1 | 1 | 2 | 3 | 5 | 8 |13 |21 |34 |55 |
단계별 계산:
dp[2] = dp[1] + dp[0] = 1 + 0 = 1
dp[3] = dp[2] + dp[1] = 1 + 1 = 2
dp[4] = dp[3] + dp[2] = 2 + 1 = 3
dp[5] = dp[4] + dp[3] = 3 + 2 = 5
...
dp[10] = dp[9] + dp[8] = 34 + 21 = 55
Plain Text
복사
완전한 피보나치 DP 구현
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n = 10; // F(10) 구하기
// DP 테이블 초기화
vector<int> dp(n + 1);
// 기저 조건
dp[0] = 0;
dp[1] = 1;
cout << "피보나치 수열 계산 과정:" << endl;
cout << "F(0) = " << dp[0] << endl;
cout << "F(1) = " << dp[1] << endl;
// Bottom-up 방식으로 계산
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i-1] + dp[i-2];
cout << "F(" << i << ") = F(" << (i-1) << ") + F(" << (i-2)
<< ") = " << dp[i-1] << " + " << dp[i-2] << " = " << dp[i] << endl;
}
cout << "\n최종 결과: F(" << n << ") = " << dp[n] << endl;
return 0;
}
C++
복사
배낭 문제 완전 정복 - 게임 아이템 수집하기
문제 상황: RPG 게임의 던전 탐험
배경 스토리 
용사가 던전을 탐험하며 보스를 처치했습니다! 보상으로 귀중한 아이템들을 발견했지만, 인벤토리 용량이 한정되어 있어 모든 아이템을 가져갈 수 없습니다.
🎒 인벤토리 용량: 5칸
💎 발견한 아이템들:
┌──────────────────────────────────────────┐
│ 💍 마법반지 │ 2칸 │ 가치 3 │ 💰💰💰 │
│ ⚔️ 신검 │ 3칸 │ 가치 4 │ 💰💰💰💰 │
│ 🛡️ 용방패 │ 4칸 │ 가치 5 │ 💰💰💰💰💰 │
│ 👑 왕의왕관 │ 5칸 │ 가치 6 │ 💰💰💰💰💰💰 │
└──────────────────────────────────────────┘
❓ 문제: 인벤토리에 담을 수 있는 최대 가치는?
Plain Text
복사
DP 테이블 설계 및 점화식
상태 정의
dp[i][w] = i번째 아이템까지 고려했을 때,
용량 w에서 얻을 수 있는 최대 가치
C++
복사
점화식
dp[i][w] = max(
dp[i-1][w], // 현재 아이템을 선택하지 않는 경우
dp[i-1][w-weight[i]] + value[i] // 현재 아이템을 선택하는 경우
// 현재무게를 뺀 나머지 무게의 가치 + 현재무게의 가치
)
C++
복사
경계 조건
if (weight[i] > w) {
dp[i][w] = dp[i-1][w]; // 용량 초과 시 선택 불가
}
C++
복사
단계별 DP 테이블 구성
아이템 정보 정리
아이템 1: 💍 마법반지 (2칸, 가치 3)
아이템 2: ⚔️ 신검 (3칸, 가치 4)
아이템 3: 🛡️ 용방패 (4칸, 가치 5)
아이템 4: 👑 왕의왕관 (5칸, 가치 6)
Plain Text
복사
초기화
용량 → 0 1 2 3 4 5
아이템 ↓
0개 (없음) 0 0 0 0 0 0
Plain Text
복사
1단계: 마법반지 (2칸, 가치 3) 추가
💭 각 용량별 계산:
- 용량 0~1: 마법반지(2칸)이 안들어감 → 0
- 용량 2~5: 마법반지 들어감 → 가치 3
용량 → 0 1 2 3 4 5
아이템 ↓
0개 (없음) 0 0 0 0 0 0
1개 (반지) 0 0 3 3 3 3
Plain Text
복사
2단계: 신검 (3칸, 가치 4) 추가
💭 핵심 계산 (용량 5):
dp[2][5] = max(
dp[1][5], // 신검 안가져가기 = 3
dp[1][5-3] + 4 // 신검 가져가기 = dp[1][2] + 4 = 3 + 4 = 7
)
→ max(3, 7) = 7 선택! (반지 + 신검)
용량 → 0 1 2 3 4 5
아이템 ↓
0개 (없음) 0 0 0 0 0 0
1개 (반지) 0 0 3 3 3 3
2개 (신검) 0 0 3 4 4 7
Plain Text
복사
3단계: 용방패 (4칸, 가치 5) 추가
💭 핵심 계산 (용량 5):
dp[3][5] = max(
dp[2][5], // 용방패 안가져가기 = 7
dp[2][5-4] + 5 // 용방패 가져가기 = dp[2][1] + 5 = 0 + 5 = 5
)
→ max(7, 5) = 7 선택! (반지 + 신검이 더 좋음)
용량 → 0 1 2 3 4 5
아이템 ↓
0개 (없음) 0 0 0 0 0 0
1개 (반지) 0 0 3 3 3 3
2개 (신검) 0 0 3 4 4 7
3개 (용방패) 0 0 3 4 5 7
Plain Text
복사
4단계: 왕의왕관 (5칸, 가치 6) 추가
💭 핵심 계산 (용량 5):
dp[4][5] = max(
dp[3][5], // 왕관 안가져가기 = 7
dp[3][5-5] + 6 // 왕관 가져가기 = dp[3][0] + 6 = 0 + 6 = 6
)
→ max(7, 6) = 7 선택! (반지 + 신검 조합이 최고)
용량 → 0 1 2 3 4 5
아이템 ↓
0개 (없음) 0 0 0 0 0 0
1개 (반지) 0 0 3 3 3 3
2개 (신검) 0 0 3 4 4 7
3개 (용방패) 0 0 3 4 5 7
4개 (왕관) 0 0 3 4 5 7 ← 최종!
Plain Text
복사
완전한 C++ 구현
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int main() {
// 게임 설정
int capacity = 5;
vector<string> items = {"💍마법반지", "⚔️신검", "🛡️용방패", "👑왕의왕관"};
vector<int> weights = {2, 3, 4, 5};
vector<int> values = {3, 4, 5, 6};
int n = items.size();
// DP 테이블 초기화
vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));
cout << "🎮 RPG 게임 - 던전 아이템 수집하기" << endl;
cout << "🎒 인벤토리 용량: " << capacity << "칸\n" << endl;
// 아이템 정보 출력
cout << "💎 발견한 아이템들:" << endl;
for (int i = 0; i < n; i++) {
cout << " " << (i+1) << ". " << items[i]
<< " (용량: " << weights[i] << "칸, 가치: " << values[i] << ")" << endl;
}
cout << endl;
// DP 계산
for (int i = 1; i <= n; i++) {
cout << "📋 " << i << "단계: " << items[i-1] << " 검토 중..." << endl;
for (int w = 0; w <= capacity; w++) {
if (weights[i-1] > w) {
// 현재 아이템이 용량을 초과하는 경우
dp[i][w] = dp[i-1][w];
} else {
// 선택하지 않는 경우와 선택하는 경우 중 최대값
int not_take = dp[i-1][w];
int take = dp[i-1][w - weights[i-1]] + values[i-1];
dp[i][w] = max(not_take, take);
// 최대 용량에서의 계산 과정 출력
if (w == capacity) {
cout << " 용량 " << w << "칸: max(" << not_take
<< ", " << take << ") = " << dp[i][w] << endl;
}
}
}
cout << endl;
}
// 최종 DP 테이블 출력
cout << "📊 최종 DP 테이블:" << endl;
cout << " ";
for (int w = 0; w <= capacity; w++) {
cout << "c" << w << " ";
}
cout << endl;
for (int i = 0; i <= n; i++) {
if (i == 0) {
cout << "없음: ";
} else {
cout << i << "개: ";
}
for (int w = 0; w <= capacity; w++) {
cout << dp[i][w] << " ";
}
cout << endl;
}
cout << "\n🏆 최대 가치: " << dp[n][capacity] << endl;
// 선택된 아이템 역추적
cout << "\n🎒 선택된 아이템들:" << endl;
vector<bool> selected(n, false);
int w = capacity;
for (int i = n; i > 0 && w > 0; i--) {
if (dp[i][w] != dp[i-1][w]) {
selected[i-1] = true;
cout << " ✅ " << items[i-1]
<< " (용량: " << weights[i-1] << "칸, 가치: " << values[i-1] << ")" << endl;
w -= weights[i-1];
} else {
cout << " ❌ " << items[i-1] << " (선택하지 않음)" << endl;
}
}
// 최종 요약
int total_weight = 0, total_value = 0;
for (int i = 0; i < n; i++) {
if (selected[i]) {
total_weight += weights[i];
total_value += values[i];
}
}
cout << "\n📋 최종 요약:" << endl;
cout << " 총 사용 용량: " << total_weight << "/" << capacity << "칸" << endl;
cout << " 총 가치: " << total_value << endl;
cout << " 남은 용량: " << (capacity - total_weight) << "칸" << endl;
return 0;
}
C++
복사
결과 분석 및 역추적
최적해: 가치 7
🎒 선택된 아이템 조합:
✅ 💍 마법반지 (2칸, 가치 3)
✅ ⚔️ 신검 (3칸, 가치 4)
❌ 🛡️ 용방패 (선택하지 않음)
❌ 👑 왕의왕관 (선택하지 않음)
총 사용 용량: 2 + 3 = 5칸 (꽉 참!)
총 가치: 3 + 4 = 7
Plain Text
복사
역추적 과정
1. dp[4][5] = 7, dp[3][5] = 7 → 왕관 선택 안함 ❌
2. dp[3][5] = 7, dp[2][5] = 7 → 용방패 선택 안함 ❌
3. dp[2][5] = 7, dp[1][5] = 3 → 신검 선택! ✅ (남은 용량: 5-3=2)
4. dp[1][2] = 3, dp[0][2] = 0 → 마법반지 선택! ✅
Plain Text
복사
왜 이 조합이 최적일까?
다른 가능한 조합들:
- 👑 왕관만: 5칸, 가치 6
- 🛡️ 용방패만: 4칸, 가치 5
- 💍 반지 + ⚔️ 신검: 5칸, 가치 7 ← 최고!
작은 아이템들의 조합이 큰 아이템 하나보다 더 효율적!
Plain Text
복사
핵심 개념 정리
1. DP의 핵심 아이디어
•
최적 부분 구조: 작은 문제의 최적해로 큰 문제의 최적해 구성
•
중복 부분 문제: 같은 계산을 여러 번 하지 않도록 결과 저장
•
점진적 구성: 아이템을 하나씩 추가하며 최적해 갱신
2. 선택의 기준
각 단계에서 두 가지 선택:
1. 현재 아이템 선택 안함 → 이전 상태 그대로
2. 현재 아이템 선택함 → 남은 공간의 최적해 + 현재 아이템 가치
Plain Text
복사
3. 시간/공간 복잡도
•
시간 복잡도: O(n × W) = O(4 × 5) = O(20)
•
공간 복잡도: O(n × W) = O(20)
4. 실전 팁
•
작은 아이템 조합 vs 큰 아이템: 효율성 비교 중요
•
역추적: 최적해가 어떤 선택들로 구성되었는지 파악
•
경계 조건: 용량 초과 시 처리 주의
세 가지 DP 문제 비교
구분 | 동전 문제 | 피보나치 | 배낭 문제 |
DP 차원 | 1차원 | 1차원 | 2차원 |
상태 정의 | dp[i] = i원을 만드는 경우의 수 | dp[i] = i번째 피보나치 수 | dp[i][w] = i개 물건, 용량w에서 최대 가치 |
점화식 | dp[i] += dp[i-coin] | dp[i] = dp[i-1] + dp[i-2] | dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight] + value) |
시간복잡도 | O(amount × coins) | O(n) | O(n × W) |
공간복잡도 | O(amount) | O(n) | O(n × W) |
DP 마스터 체크리스트
문제 분석 단계
1.
중복 부분 문제가 있는가?
•
같은 하위 문제가 여러 번 계산되는지 확인
2.
최적 부분 구조를 만족하는가?
•
작은 문제의 최적해가 큰 문제의 최적해에 포함되는지 확인
3.
무후효성을 만족하는가?
•
현재 상태가 과거 경로와 무관하게 결정되는지 확인
구현 전략
1.
Top-down (재귀 + 메모이제이션)
int dp[MAX];
int solve(int n)
{
if (dp[n] != -1) return dp[n]; // 이미 계산됨
return dp[n] = /* 점화식 */;
}
C++
복사
2.
Bottom-up (반복문)
vector<int> dp(n + 1);
dp[0] = /* 초기값 */;
for (int i = 1; i <= n; i++)
{
dp[i] = /* 점화식 */;
}
C++
복사
최적화 기법
1.
공간 최적화: 필요한 이전 상태만 저장
2.
상태 압축: 2차원을 1차원으로 변환
3.
계산 순서 최적화: 불필요한 계산 생략
실전 DP 문제 해결 전략
문제 접근 순서
1.
브루트포스 솔루션 먼저 생각
•
모든 경우를 다 해보는 재귀 함수 작성
•
시간복잡도가 너무 크다면 DP 고려
2.
상태와 점화식 정의
•
dp[i]가 무엇을 나타내는지 명확히 정의
•
상태 간의 관계식 도출
3.
기저 조건 설정
•
가장 작은 문제의 답을 직접 설정
•
경계 조건 처리
4.
계산 순서 결정
•
의존성을 고려한 계산 순서
•
Bottom-up vs Top-down 선택
디버깅 팁
1.
작은 케이스로 손 계산: 점화식이 맞는지 검증
2.
DP 테이블 출력: 중간 과정 확인
3.
경계 조건 체크: 배열 범위 초과 방지
4.
초기값 확인: 기저 조건이 올바른지 검토
핵심 정리
1.
DP = 과거의 최적해 + 현재 선택 = 새로운 최적해
2.
테이블 크기: 보통 문제 범위 + 1
3.
점화식이 핵심: 상태 간의 관계를 정확히 파악
4.
초기값 중요: 기저 조건을 올바르게 설정
현실적인 DP 학습 과정:
초보자의 전형적인 시행착오:
문제를 본다 → "어... 뭔가 복잡하네?"
→ 그리디로 시도 → 반례 발견 😱
→ 브루트포스로 시도 → 시간초과 😵
→ "이게 뭔 문제지?" → 답 확인 → "아, DP구나..."
Plain Text
복사
점진적 감각 발달:
5-10문제 후: "어? 이거 아까 본 패턴 같은데?"
20-30문제 후: "이런 키워드 나오면 DP일 확률 높아"
50문제 후: "문제 읽자마자 DP 냄새가 난다"
100문제 후: "이건 1차원 DP, 저건 2차원 DP"
Plain Text
복사
실제 도움되는 학습법:
의도적으로 실패해보기
// 1. 일부러 재귀로 먼저 풀어보기
int slowFib(int n)
{
if (n <= 1) return n;
return slowFib(n-1) + slowFib(n-2);
}
// 2. "어? 너무 느리네" 경험하기
// 3. 같은 계산 반복 발견하기
// 4. "아하! 이걸 저장하면 되겠다" 깨달음
C++
복사
문제 유형별 외우기
그냥 외워버리는 것도 방법:
- "경우의 수" → DP 의심
- "최소 개수" → DP 의심
- "배낭, 동전, 계단" → 무조건 DP
- "피보나치 같은 점화식" → DP
Plain Text
복사
정말 솔직히 말하면, 처음 20-30문제는 답 보고 "아, 이런 게 DP구나" 하면서 패턴을 익히는 게 가장 효율적입니다!
무작정 고민하다가 시간만 낭비하는 것보다는, 답을 보고 "왜 DP인지" 이해하는 게 더 도움이 돼요. 

“강의는 많은데, 왜 나는 아직도 코드를 못 짤까?”
혼자 공부하다 보면 누구나 이런 고민을 하게 됩니다.
•
강의는 다 들었지만 막상 손이 안 움직이고,
•
복습을 하려 해도 무엇을 다시 봐야 할지 모르겠고,
•
질문할 곳도 없고,
•
유튜브는 결국 정답을 따라 치는 것밖에 안 되는 것 같고.
문제는 ‘연습’이 빠졌기 때문입니다.
단순히 강의를 듣는 것만으로는 실력이 늘지 않습니다.
실제 문제를 풀고, 고민하고, 직접 구현해보는 시간이 반드시 필요합니다.
그래서, 얌얌코딩 코칭은 다릅니다.
그냥 가르치지 않습니다.
스스로 설계하고, 코딩할 수 있게 만듭니다.
얌얌코딩 코칭에서는 단순한 예제가 아닌,
스스로 문제를 분석하고 구현해야 하는 연습문제를 제공합니다.
이 연습문제들은 다음과 같은 역량을 키우기 위해 설계되어 있습니다:
•
문제를 스스로 쪼개고 설계하는 힘
•
다양한 조건을 만족시키는 실제 구현 능력
•
기능 단위가 아닌, 프로그램 단위로 사고하는 습관
•
마침내 자신의 힘으로 코드를 끝까지 작성하는 경험
지금 필요한 건 더 많은 강의가 아닙니다.
코드를 스스로 완성해 나가는 훈련,
그것이 지금 실력을 끌어올릴 가장 현실적인 방법입니다.