동적계획법(DP)
동적계획법(dynamic programming)은 자주 볼 수 있는 디자인 패러다임 중에 하나입니다. 동적 계획법에서는 하나의 큰 문제를 여러 개의 작은 문제로 나눈 뒤, 그 결과를 저장하여 다시 큰 문제를 해결할 때 사용합니다.
동적계획법을 왜 사용하는 걸까요? 일반적인 재귀 방식 또한 DP와 유사하다고 볼 수 있습니다. 그러나 재귀를 단순히 사용할 시 동일한 작은 문제들이 여러 번 반복되어 비효율적으로 계산됩니다.
가장 많이 쓰이는 예시는 피보나치 수열입니다. 피보나치 수열을 재귀로 구현할 시 return f(n) = f(n-1) + f(n-2)이라는 단순한 식이 나옵니다.
이렇게 구현할 시, 위의 트리와 같은 호출이 일어납니다. 같은 값을 구하는 과정이 반복되서 일어나는 것을 확인할 수 있습니다. 그러나 DP를 이용하여 한 번 구한 작은 문제의 결과값을 저장해두고 사용하면 반복하여 계산할 필요가 없습니다.
동적계획법의 사용 조건
동적계획법의 사용 조건은 크게 두 가지입니다.
- Overlapping Subproblems (겹치는 부분 문제): 동일한 작은 문제들이 반복해서 나타나는 경우에 사용 가능
- Optimal Substurcture (최적 부분 구조): 부분 문제의 최적 결과 값을 사용해 전체 문제의 최적 결과를 낼 수 있는 경우에 사용 가능
동적계획법의 접근 방식
그럼, 문제에 동적계획법을 사용하려면 어떻게 접근해야 할까요? 이러한 방식은 Bottom-Up 방식과 Top-Down 방식으로 나뉩니다.
Bottom-Up 방식은 아래에서부터 계산을 수행하고 값을 누적시켜서 전체 큰 문제를 해결합니다. 반복문을 사용하는 방식에 해당됩니다.
결과값을 계산하기 위한 일차원 배열인 dp를 만들고, dp[n]이 목표로 구하는 값이라고 합시다. 그럼 Bottom-Up 방식에서는 dp[0]에서부터 시작하여 반복문을 통해 dp[n]까지 구합니다.
이와 반대로 Top-Down 방식은 큰 문제에서 작은 문제로 나눠가며, 재귀 호출을 이용해 문제를 풀게 됩니다.
BOJ 동적계획법 문제
BOJ 11726 2xn 타일링
문제 요약
- 입력: n
- 2xn 크기의 직사각형을 1x2, 2x1 타일로 채우는 방법의 수를 구하기
- 출력: 2xn 크기의 직사각형을 채우는 방법의 수를 10,007로 나눈 나머지
풀이 과정
해당 문제에서의 점화식은 dp[n] = dp[n-1] + dp[n-2] 입니다. 또한 2x1 크기일 때의 경우의 수는 1, 2x2 크기일 때는 2이므로 dp[1] = 1, dp[2] = 2라고 볼 수 있습니다.
10,007로 나누지 않을 경우 overflow 가능성이 있기 때문에 값을 저장할 때부터 10,007로 나누어 줍니다.
코드
// BOJ 11726 2xn 타일링
#include <iostream>
#include <vector>
using namespace std;
int main()
{
ios_base ::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int n;
vector<int> dp;
cin >> n;
dp.push_back(1);
dp.push_back(2);
for(int i=2; i<n; i++){
dp.push_back((dp[i-1]+dp[i-2]) % 10007);
}
cout << dp[n-1];
return 0;
}
BOJ 11055 가장 큰 증가하는 부분 수열
문제 요약
- 입력: 수열 A의 크기 N, 수열 A를 이루고 있는 A_j
- 출력: 수열 A의 합이 가장 큰 증가하는 부분 수열의 합
풀이 과정
해당 문제를 풀기 위해 dp[i]를 A[i]를 증가하는 부분 수열의 마지막 요소라고 할 때의 부분 수열의 합이라고 정의합니다. 현재 A[i]보다 앞 순서에서 A[i]보다 작은 값(A[j])이 있고, dp[j]가 dp[i]보다 크다면 dp[i]를 dp[j]로 변경해줍니다. 이렇게 for문을 돌고 난 후에 dp[i]에 현재 위치 값인 A[i]를 더해줍니다. 합이 가장 큰 부분 수열의 합을 구하기 위해서 현재 구한 dp[i]값과 이전 res 값을 비교하여 더 큰 값을 저장합니다.
코드
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
ios_base ::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int N, A[1001], dp[1001] = {0,}, res = 0;
cin >> N;
for (int i = 1; i <= N; i++)
{
cin >> A[i];
}
for (int i = 1; i <= N; i++)
{
for (int j = 1; j < i; j++)
{
if (A[j] < A[i])
dp[i] = max(dp[i], dp[j]);
}
dp[i] += A[i];
res = max(dp[i], res);
}
cout << res;
return 0;
}
BOJ 9465 스티커
문제 요약
- 입력: 테스트 케이스의 개수 T, n, n개의 정수
- 스티커 한 장을 떼면, 그 스티커와 변을 공유하는 스티커는 모두 찢어져서 사용할 수 없게 됨
- 출력: 2n개의 스티커 중에서 두 변을 공유하지 않는 스티커 점수의 최댓값
풀이 과정
해당 문제를 풀기 위해 dp[0][i]를 윗줄 i번째 스티커를 택했을 때의 최댓값, dp[1][i]를 아랫줄 i번째 스티커를 택했을 때의 최댓값으로 정의합니다. 점화식을 세워보면 dp[0][i] = max(dp[1][i-1], dp[1][i-2]) + dp[0][i], dp[1][i] = max(dp[0][i-1], dp[0][i-1]) + dp[1][i]라는 식이 나옵니다.
코드
// BOJ 9465 스티커
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
ios_base ::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int T;
cin >> T;
while (T--)
{
int n;
cin >> n;
int dp[2][100001] = {0, };
dp[0][0] = dp[1][0] = 0;
for (int i = 1; i <= n; i++)
{
cin >> dp[0][i];
}
for (int i = 1; i <= n; i++)
{
cin >> dp[1][i];
}
for (int i = 2; i <= n; i++)
{
dp[0][i] = max(dp[1][i - 1], dp[1][i - 2]) + dp[0][i];
dp[1][i] = max(dp[0][i - 1], dp[0][i - 2]) + dp[1][i];
}
int res = max(dp[1][n], dp[0][n]);
cout << res << "\n";
}
return 0;
}
BOJ 9084 동전
문제 요약
- 입력: 테스트 케이스의 개수 T, 동전의 가짓수 N, N가지 동전의 각 금액이 오름차순으로, 주어진 N가지 동전으로 만들어야 할 금액 M
- 출력: N가지 동전으로 금액 M을 만드는 모든 방법의 수를 한 줄에 하나씩 출력
풀이 과정
해당 문제를 풀기 위해 dp[i]를 i원을 만들 수 있는 방법의 수로 정의하면, dp[i] = dp[j] + dp[j-coin[i]]라는 점화식을 세울 수 있습니다.
코드
// BOJ 9084 동전
#include <iostream>
using namespace std;
int main()
{
ios_base ::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
int T;
cin >> T;
while (T--)
{
int N, M, dp[10001] = {
0,
},
coin[21];
cin >> N;
for (int i = 1; i <= N; i++)
{
cin >> coin[i];
}
cin >> M;
dp[0] = 1;
for (int i = 1; i <= N; i++)
{
for (int j = coin[i]; j <= M; j++)
{
dp[j] = dp[j] + dp[j - coin[i]];
}
}
cout << dp[M] << "\n";
}
return 0;
}
'Group Study (2022-2023) > Coding Test' 카테고리의 다른 글
[Coding Test] 10주차 - 문자열 (0) | 2023.03.27 |
---|---|
[Coding Test] 7주차 - 이분탐색 (0) | 2023.03.05 |
[Coding Test] 6주차 - 정렬 (1) | 2023.02.25 |
[Coding Test] 5주차 - DFS와 BFS (0) | 2023.02.19 |
[Coding Test] 4주차 - Implementation (0) | 2023.02.10 |