April 14, 2020
Knapsack Problem, 배낭문제는 다이나믹 프로그래밍에서 매우 유명한 문제이다. 어떤 배낭이 있고 그 배낭안에 넣을 수 있는 최대 무게가 K라고 하자. 배낭에 넣을 수 있는 N개의 물건이 각기 다른 가치 V를 가지고 있고 각 물건마다 다른 무게 W를 가지고 있을 때, 배낭이 최대한 가치가 높은 물건들을 담을 수 있는 조합을 찾는 문제이다. 냅색 문제는 Fractional Knapsack, 즉 물건을 쪼갤 수 있어 무게나 가치가 소수점 단위로 나뉘는 문제, 또는 0-1 Knapsack 물건을 쪼갤 수 없어 무게와 가치가 무조건 정수형태를 가지는 두 유형으로 나뉜다. 이번에는 0-1 Knapsack 문제를 해결하는 방법에 대해서 생각해보자.
만약 n개의 물건이 있을 때, 가능한 모든 조합을 만들기 위해서는 2n 개의 경우의 수를 모두 시도해 보아야한다. 가지고 있는 물건이 몇개 없다면 별로 시간이 오래걸리지는 않겠지만, 2n 은 물건의 갯수가 하나만 늘어도 경우의 수가 크게 늘어나게 된다. 이 연산을 수행하려면 𝛩(2n ) 의 시간을 사용해야하고, 프로그래머 입장에서 그다지 좋은 접근은 아닐 것이다.
따라서 이 문제는 당연스럽게도 다이나믹 프로그래밍으로 해결해야 한다. 그런데 항상 그렇듯 DP 의 가장 큰 어려움은 subproblem 을 정의하는 것이 쉽지 않다는 것이다. 나도 그랬고 많은 사람들이 이 문제의 subproblem 을 찾기 위해 다음과 같이 생각했을 것이다.
이 수업을 듣기전에 나도 이렇게 생각했고 이 접근으로 문제를 풀어보려고 몇시간을 고민했던 적도 있다. 하지만 이 접근은 subproblem을 만들어내지 못한다. 기존에 있던 조합에서 하나의 물건을 빼고 다른 하나를 넣는 것으로 최대가치를 만들수 있기 때문이다. 따라서 무게 4일 때의 조합과 무게 5일때의 조합이 달라질 여지가 충분히 있는 것이다.
위 접근이 아닌 다른 방법으로 어떻게 접근해야 할까. 배낭에 물건을 넣을 때, 우리가 선택할 수 있는 경우는 두 가지가 있다.
만약 현재 내가 넣으려고 하는 물건의 무게가 전체 제한무게를 초과한다면, 선택지는 물건을 넣지 않는 것 밖에 없다. 따라서 다음과 같은 식을 일단 하나 만들 수 있다.
B는 DP를 위한 최대가치를 담는 배열이고, k는 현재 넣은 물건의 번호, W은 최대 무게를 의미한다. wk 는 k번째 물건에 대한 무게를 의미한다. 따라서 위 식은 k번째 물건이 총 제한 무게를 초과한다면 k번째 최대가치를 바로 k-1번째 물건을 넣을 경우의 가치로 정하는 것이다.
만약 물건을 넣을 수 있는 무게가 조금이라도 남았다면 어떻게 해야할까? 이때 우리의 선택지는 다시 두 가지로 나뉜다.
우리는 최대 가치를 원하기 때문에, 두 경우 중 더 큰 값을 선택하게 될 것이다. 이것을 수식으로 만들면,
현재 물건을 넣기 위해 넣었던 물건을 뺐을 때의 가치에 현재 물건의 가치를 더해주면 된다.
표를 채워가면서 더 자세하게 생각해보자. 우리에게 네 개의 물건이 있다고 하자 물건은 (무게, 가치) 로 나타내겠다. 그리고 물건을 넣을 가방의 최대 무게는 5라고 하자.
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | |||||
| 1 | |||||
| 2 | |||||
| 3 | |||||
| 4 | |||||
| 5 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | ||||
| 2 | 0 | ||||
| 3 | 0 | ||||
| 4 | 0 | ||||
| 5 | 0 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | |||
| 2 | 0 | ||||
| 3 | 0 | ||||
| 4 | 0 | ||||
| 5 | 0 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | |||
| 2 | 0 | 3 | |||
| 3 | 0 | ||||
| 4 | 0 | ||||
| 5 | 0 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | |||
| 2 | 0 | 3 | |||
| 3 | 0 | 3 | |||
| 4 | 0 | 3 | |||
| 5 | 0 | 3 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | ||
| 2 | 0 | 3 | 3 | ||
| 3 | 0 | 3 | |||
| 4 | 0 | 3 | |||
| 5 | 0 | 3 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | ||
| 2 | 0 | 3 | 3 | ||
| 3 | 0 | 3 | 4 | ||
| 4 | 0 | 3 | |||
| 5 | 0 | 3 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | ||
| 2 | 0 | 3 | 3 | ||
| 3 | 0 | 3 | 4 | ||
| 4 | 0 | 3 | 4 | ||
| 5 | 0 | 3 | 7 |
| 0 | 1 (2, 3) | 2 (3, 4) | 3 (4, 5) | 4 (5, 6) | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | 3 | 3 | 3 | 3 |
| 3 | 0 | 3 | 4 | 4 | 4 |
| 4 | 0 | 3 | 4 | 5 | 5 |
| 5 | 0 | 3 | 7 | 7 | 7 |
위 로직을 pseudo code로 적어보자.
for w = 0 to W
B[0][W] = 0
for i = 1 to n
B[i][0] = 0
for w = 1 to W
if w[i] <= w
if b[i] + B[i-1][w-w[i]] > B[i-1][w]
B[i][w] = b[i] + B[i-1][w-w[i]]
else B[i][w] = B[i-1][w]
else
B[i][w] = B[i-1][w]책에 나와있는 코드인데 변수가 무엇을 의미하는지 정확히 안나와 있어서 잘 와닿지 않는다. 백준에 동일한 알고리즘 문제가 있으니 [백준 알고리즘] 문제를 풀어보자. vector, pair, 같은 컨테이너나 algorithm에 들어있는 max 함수를 사용하면 더 수월하겠지만 최대한 pseudo code랑 비슷하게 만들었다. PS 포스트는 STL로 더 깔끔한 코드로 만들어서 올려보자.
문제 이 문제는 아주 평범한 배낭에 관한 문제이다.
한 달 후면 국가의 부름을 받게 되는 준서는 여행을 가려고 한다. 세상과의 단절을 슬퍼하며 최대한 즐기기 위한 여행이기 때문에, 가지고 다닐 배낭 또한 최대한 가치 있게 싸려고 한다.
준서가 여행에 필요하다고 생각하는 N개의 물건이 있다. 각 물건은 무게 W와 가치 V를 가지는데, 해당 물건을 배낭에 넣어서 가면 준서가 V만큼 즐길 수 있다. 아직 행군을 해본 적이 없는 준서는 최대 K무게까지의 배낭만 들고 다닐 수 있다. 준서가 최대한 즐거운 여행을 하기 위해 배낭에 넣을 수 있는 물건들의 가치의 최댓값을 알려주자.
입력 첫 줄에 물품의 수 N(1 ≤ N ≤ 100)과 준서가 버틸 수 있는 무게 K(1 ≤ K ≤ 100,000)가 주어진다. 두 번째 줄부터 N개의 줄에 거쳐 각 물건의 무게 W(1 ≤ W ≤ 100,000)와 해당 물건의 가치 V(0 ≤ V ≤ 1,000)가 주어진다.
입력으로 주어지는 모든 수는 정수이다.
#include <cstdio>
#define WEIGHT 0
#define VALUE 1
using namespace std;
int main()
{
int dp[101][100001] = {0, };
int item[101][2];
int N, K;
scanf("%d %d", &N, &K);
for (int i = 1; i <= N; i++){
scanf("%d %d", &item[i][WEIGHT], &item[i][VALUE]);
}
for (int i = 1 ; i <= N ; i++){
for (int w = 1 ; w <= K ; w++){
if (item[i][WEIGHT] <= w){ // 최대 무게를 하나씩 올려가면서 계산
if ((item[i][VALUE] + dp[i-1][w-item[i][WEIGHT]]) > dp[i-1][w]) // 이전 최대가치를 사용하는 것보다 이전걸 빼고 현재물건을 넣는게 더 좋다면 넣어주자
dp[i][w] = item[i][VALUE] + dp[i-1][w-item[i][WEIGHT]];
else dp[i][w] = dp[i-1][w]; // 아니라면 이전가치를 그대로 사용
}
else{
dp[i][w] = dp[i-1][w];
}
}
}
printf("%d", dp[N][K]);
return 0;
}DP 로 접근한 Knapsack Problem의 time complexity는 단순하게 2차원 배열을 모두 채워주면 끝나기 때문에, 무게 W, 물건의 갯수 N에 대해 Օ(NW) 의 시간복잡도를 가진다.