배낭 문제란?
배낭 문제(Knapsack Problem)는 제한된 용량의 배낭에 가치가 서로 다른 물건들을 어떻게 담아야 최대 가치를 얻을 수 있는가를 묻는 최적화 문제다. 이사할 때 트렁크 용량이 한정되어 있는데, 짐마다 중요도가 다르다면 무엇부터 넣어야 할지 고민하는 상황과 같다.
배낭 문제는 크게 두 가지 버전으로 나뉜다.
1. 0-1 Knapsack Problem
- 물건을 통째로 넣거나 빼거나 둘 중 하나다. 쪼갤 수 없다.
- Dynamic Programming(DP) 으로 풀어야 하며, Greedy로는 최적해를 보장할 수 없다.
2. Fractional Knapsack Problem
- 물건을 원하는 만큼 쪼개서 넣을 수 있다.
- 각 물건의 단위 무게당 가치(value per weight, ) 를 계산한 뒤, 가치가 높은 것부터 최대한 많이 담는 Greedy 전략으로 최적해를 구할 수 있다.
두 문제 모두 최적 부분 구조(Optimal Substructure) 를 가진다. 하지만 탐욕 선택 속성(Greedy Choice Property) 은 Fractional Knapsack에만 성립하기 때문에, 0-1 Knapsack에는 Greedy를 적용할 수 없다.
0-1 Knapsack Problem

0-1 Knapsack은 물건을 쪼갤 수 없으므로 각 물건에 대해 "넣는다 / 넣지 않는다"의 이진 선택만 가능하다. 이 문제를 해결하는 네 가지 접근법을 살펴본다.
Brute Force
가장 직관적인 방법은 모든 조합을 시도하는 것이다. n개의 물건이 있으면 각각을 넣거나 빼는 가지 조합이 존재한다. 이 중에서 총 무게가 W 이하이면서 총 가치가 최대인 조합을 찾으면 된다.
당연히 시간 복잡도는 으로, 물건의 수가 조금만 늘어나도 현실적으로 사용할 수 없다.
Greedy
Greedy 전략은 0-1 Knapsack에서는 최적해를 보장하지 못한다. 단위 가치가 높은 물건부터 담는다고 해서 반드시 전체 최적해가 되지는 않기 때문이다.
Dynamic Programming
0-1 Knapsack의 실질적인 해법은 DP(Dynamic Programming) 다. DP에서 가장 중요한 것은 부분 문제(Subproblem)를 올바르게 정의하는 것이다.
부분 문제 정의: 첫 번째 시도
물건에 1부터 n까지 번호를 매기고, = {물건 1, 2, ..., k}로 정의해 보자. 이는 가장 단순한 부분 문제 정의이긴 하지만, 최종 해 을 부분 문제 로 표현하려고 하면 문제가 생긴다.

위 그림에서 볼 수 있듯이, 물건 집합만으로는 부분 문제 간의 관계를 제대로 기술할 수 없다. 이 정의에는 결함(flaw) 이 있으므로 새로운 정의가 필요하다.
부분 문제 정의: 매개변수 추가
해결책은 무게(w) 라는 매개변수를 추가하는 것이다. 부분 문제를 B[k, w] 로 정의하면, "물건 1~k 중에서 총 무게가 w 이하가 되도록 골랐을 때의 최대 가치"를 나타낸다.

의 최적 부분 집합이 총 무게 W를 가질 때, 물건 k를 포함하거나 포함하지 않거나 둘 중 하나다.
- Case 1: > W 이면, 물건 k는 배낭에 들어갈 수 없다. 따라서 B[k, w] = B[k-1, w]가 된다.
- Case 2: ≤ W 이면, 물건 k를 넣을 수도 있다. 넣는 경우와 넣지 않는 경우 중 더 큰 가치를 선택한다.
이를 의사 코드로 표현하면 다음과 같다.
for w = 0 to W
B[0, w] = 0 // 물건이 0개면 가치도 0
for i = 1 to n
B[i, 0] = 0 // 무게 한도가 0이면 아무것도 못 넣음
for w = 1 to W
if w[i] <= w // 물건 i를 넣을 수 있는 경우
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] // w[i] > w, 넣을 수 없음
시간 복잡도는 O(nW) 다. n은 물건의 수, W는 배낭의 용량이다. 주의할 점은 이것이 의사 다항(Pseudo-polynomial) 시간이라는 것이다. W의 값 자체에 비례하므로 W가 매우 크면 비효율적일 수 있다.
Branch and Bound (Best-First Search)
Branch and Bound는 탐색 트리에서 모든 노드를 방문하지 않고, 유망하지 않은 가지를 가지치기(pruning)하여 효율적으로 최적해를 찾는 방법이다.
전제 조건
물건들을 단위 무게당 가치() 기준 내림차순으로 정렬한다.
노드의 지역 변수
각 노드에서 세 가지 지역(local) 값을 계산한다.
- benefit: 현재 노드까지 담은 물건들의 가치 합
- weight: 현재 노드까지 담은 물건들의 무게 합
- bound: 이 노드에서 더 확장했을 때 얻을 수 있는 가치의 상한(upper bound). 미래를 예측하는 값이다.
Bound 계산
bound는 현재까지의 benefit에, 남은 용량을 단위 가치가 높은 물건들로 (분할해서) 채웠을 때의 가치를 더한 값이다. tot_weight = weight(이미 담은 총 무게) + 앞으로 담을 무게이며, 이때 tot_weight ≤ W를 만족해야 한다.


전역 변수: max_benefit
지역 변수 외에 하나의 전역(global) 변수인 max_benefit이 필요하다. 이는 지금까지 찾은 최적 해의 가치를 나타낸다.
- 초기값: max_benefit = 0
- weight < W이면, max_benefit = max(max_benefit, benefit)으로 갱신한다.
유망성 판단 (Promising vs. Non-promising)
지역 변수 bound와 전역 변수 max_benefit의 관계로 노드의 유망성을 판단한다.
- Non-promising (가지치기 대상):
- bound ≤ max_benefit 또는 weight > W
- Promising (확장 대상):
- bound > max_benefit 그리고 weight ≤ W
Non-promising 노드가 발생하는 두 가지 경우를 좀 더 자세히 보면 다음과 같다.
Case 1: bound ≤ max_benefit
이 노드에서 계산된 benefit 자체는 유효하다. 다만 이 노드 이후를 더 탐색해도 현재 최적해보다 나은 결과를 얻을 수 없으므로, 더 이상 확장할 필요가 없다.
Case 2: weight > W
배낭 용량을 초과했으므로 이 노드의 benefit은 유효하지 않다. max_benefit도 갱신되지 않으며, 당연히 확장도 하지 않는다.
탐색 전략
기본적으로 BFS(너비 우선 탐색) 를 적용하되, 유망한(promising) 노드 중에서 bound가 가장 큰 노드를 우선적으로 확장한다. 이것이 Best-First Search 전략이다.
예제
다음은 Branch and Bound를 단계별로 적용한 예제다.








연습 문제
1. Knapsack Greedy의 수행 시간
Fractional Knapsack에서 Greedy 알고리즘의 수행 시간은 이다.
- 단위 가치 기준으로 정렬:
- 정렬된 물건을 1부터 n-1까지 순회하며 담기:
정렬이 지배적이므로 전체 시간 복잡도는 이다. 참고로, 을 보장하는 정렬 알고리즘은 Merge Sort와 Heap Sort뿐이다.
2. Knapsack DP 예제
조건: n = 4, W = 4, 물건들의 (무게, 가치) = (2, 3), (3, 4), (4, 5), (5, 6)


참고로, DP는 정렬이 필요 없지만, Greedy는 단위 가치 기준 정렬이 반드시 필요하다.
3. Knapsack Branch and Bound 예제
배낭 용량(Total weight) = 10
| i | |||
|---|---|---|---|
| 1 | 30 | 2 | 15 |
| 2 | 60 | 5 | 12 |
| 3 | 70 | 7 | 10 |
| 4 | 20 | 4 | 5 |
위 표는 기준 내림차순으로 정렬된 상태다.

HGU 전산전자공학부 용환기 교수님의 23-1 알고리듬 분석 수업을 듣고 작성한 포스트이며, 첨부한 모든 사진은 교수님 수업 PPT의 사진 원본에 필기를 한 수정본입니다.