골드5 : 동적 계획법 문제이다.

생각

결국 무게 제약이 있는 문제이다. 나는 이 문제를 풀 때 감을 잡기 참어려웠는데, 무게에 제약이 있다는 점을 갖고 완전 탐색을 한다는 아이디어를 가지고 생각했다. 완전 탐색을 곧이곧대로 하려면 의 시간 복잡도가 생겨 풀기 어렵다. 그렇기 때문에 이 문제는 다이나믹으로 접근한다.

다이나믹으로 접근하면, 작은 문제를 정의하고 이것이 큰 문제에 어떠한 영향을 주는 지를 확인해야 한다.

정의

dp[i][j] = i번째 물건까지 포함했을 때 j의 무게를 포함하는 가치의 최댓값

말이 좀 어렵다. 예시를 보자

4 7
6 13
4 8
3 6
5 12

현재 무게 : 6 현재 가치 : 13
  1   2   3   4   5   6   7
  0   0   0   0   0  13  13

현재 무게 : 4 현재 가치 : 8
  1   2   3   4   5   6   7
  0   0   0   8   8  13  13

현재 무게 : 3 현재 가치 : 6
  1   2   3   4   5   6   7
  0   0   6   8   8  13  14

현재 무게 : 5 현재 가치 : 12
  1   2   3   4   5   6   7
  0   0   6   8  12  13  14

14

새로운 아이템이 추가됨에 따라, 그 상태에서 가질 수 있는 가치의 최댓값을 가지고 있으면 문제는 해결된다.

점화식

dp[i][j] = max(dp[i][j], dp[i-1]j-a[i][0]]] + a[i][1];

위의 예시를 잘 보면, dp[4][7]을 업데이트 하기 위해서는 dp[3번째 무게까지 포함][4의 무게] + 6(3의 가치)dp[3번째 무게까지 포함][7의 무게] 두개를 비교해야 한다. 즉, 8+6, 과 13을 비교한다.

잘 생각해보면, 이전 무게까지 포함했을 때 최댓값과, 새로 들어온 무게를 만들었을 때의 가치를 비교하면 되는 문제이다.

Code

#include<iostream>
#include<vector>
#include<string>
#include<cstring>
#include<iomanip>
#include<cmath>
#include<algorithm>
#include<queue>
 
using namespace std;
int N, K;
int a[101][2];
int dp[101][100001];
 
void print(int i){
    cout << "현재 무게 : " << a[i][0] << " 현재 가치 : " << a[i][1] << '\n';
    for (int j = 1; j <= K; j++) {
        cout << setw(3) << j << " ";
    }cout << '\n';
    for (int j = 1; j <= K; j++) {
        cout << setw(3) << dp[i][j] << " ";
    }cout << '\n' << '\n';
}
 
int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    cin >> N >> K;
    for (int i = 1; i <= N; i++) {
        int w, v;
        cin >> w >> v;
        a[i][0] = w;
        a[i][1] = v;
    }
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= K; j++) {
            // 만들고 싶은 무게에 지금 넣고 싶은 무게를 넣지 못하는 경우
            if (j - a[i][0] < 0) {
                // 이전 아이템까지 넣었던 최대 가치로 갖고 있는다
                dp[i][j] = max(dp[i][j], dp[i-1][j]);
                continue;
            }
            // 현재 만들고 싶은 무게에 지금 넣고 싶은 무게를 넣을 수 있다면
            // 이전 아이템까지 넣었을 때 최대 가치를 가졌던 것과, 지금 넣고 싶은 무게를 넣었을 때
            // 가치를 비교하여 최대 가치로 업데이트 해준다.
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-a[i][0]] + a[i][1]);
        }
        print(i);
    }
    // 최종적으로 N개의 짐까지 다 넣어보았을 때, 최대 가치가 얼마인지 찾는다.
    cout << *max_element(dp[N], &dp[N][K+1]) << '\n';
    //dp[i][j] = i번째 물건까지 포함했을 때 j의 무게를 만들었을 때 무게의 최댓값
    // dp[i][j] = max(dp[i][j], dp[i-1][j-a[i].first] + a[i].second;
 
    return 0;
}

공간 복잡도 감소

그런데, 지금 dp의 크기를 보면 100*100000 = 10000000이라 많은 메모리를 낭비하고 있다. dp[i]~dp[i+1] 과의 관계에서 값이 도출이 되기 때문에 이전 depth의 dp는 필요가 없다. 따라서 temp라는 배열에 이를 저장하고 그 저장한 배열에서 값을 따오면 공간 복잡도를 줄일 수 있다.

#include<iostream>
#include<vector>
#include<string>
#include<cstring>
#include<iomanip>
#include<cmath>
#include<algorithm>
#include<queue>
 
using namespace std;
int N, K;
int a[101][2];
int dp[100001];
int temp[100001];
 
void print(int i){
    cout << "현재 무게 : " << a[i][0] << " 현재 가치 : " << a[i][1] << '\n';
    for (int j = 1; j <= K; j++) {
        cout << setw(3) << j << " ";
    }cout << '\n';
    for (int j = 1; j <= K; j++) {
        cout << setw(3) << dp[j] << " ";
    }cout << '\n' << '\n';
}
 
int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    cin >> N >> K;
    for (int i = 1; i <= N; i++) {
        int w, v;
        cin >> w >> v;
        a[i][0] = w;
        a[i][1] = v;
    }
    for (int i = 1; i <= N; i++) {
        memcpy(temp, dp, sizeof(dp));
        for (int j = 1; j <= K; j++) {
            if (j - a[i][0] < 0) dp[j] = max(dp[j], dp[j]);
            else dp[j] = max(temp[j], temp[j-a[i][0]] + a[i][1]);
        }
    }
    cout << *max_element(dp, dp+K+1) << '\n';
    return 0;
}

Reference