5-6. DP: 배낭 문제
1. 배낭 문제 소개
배낭 문제는 전형적인 조합 최적화 문제로, 주어진 제약 조건 하에서 가장 가치 있는 조합을 찾는 문제입니다. 이 문제는 현실 세계의 다양한 문제를 모델링하는 데 사용될 수 있으며, 컴퓨터 과학 분야에서 매우 중요한 알고리즘 문제입니다. 예를 들어, 여행을 갈 때 배낭에 넣을 수 있는 물건의 무게 제한이 있고, 각 물건마다 가치와 무게가 다를 때, 가장 가치 있는 물건들을 선택하는 문제를 배낭 문제로 표현할 수 있습니다.
1) 문제 정의
배낭 문제는 다음과 같이 정의됩니다.
- 입력:
n개의 물건 (각 물건i는 가치v_i와 무게w_i를 가짐)- 배낭의 최대 허용 무게
W
- 출력: 배낭에 담을 물건들의 부분 집합 중, 총 무게가
W를 초과하지 않으면서, 총 가치가 최대가 되도록 하는 부분 집합.
2) 문제 유형
배낭 문제는 여러 변형이 존재하지만, 가장 기본적으로는 다음과 같은 두 가지 유형으로 나눌 수 있습니다.
- 0/1 배낭 문제 (0/1 Knapsack Problem): 각 물건을 배낭에 넣거나 넣지 않거나, 즉 0개 또는 1개만 넣을 수 있습니다.
- 분할 가능한 배낭 문제 (Fractional Knapsack Problem): 물건을 쪼개서 넣을 수 있습니다. 예를 들어, 금괴를 배낭에 넣을 때, 금괴의 일부분만 넣는 것이 가능합니다.
이 포스트에서는 0/1 배낭 문제에 초점을 맞춰 설명하겠습니다. 분할 가능한 배낭 문제는 그리디 알고리즘으로 효율적으로 해결할 수 있지만, 0/1 배낭 문제는 동적 프로그래밍(DP)을 사용해야 효과적으로 해결할 수 있습니다.
2. 0/1 배낭 문제의 DP 풀이
0/1 배낭 문제는 DP를 사용하여 효율적으로 해결할 수 있습니다. DP는 큰 문제를 작은 하위 문제로 나누어 해결하고, 하위 문제의 해결 결과를 저장하여 중복 계산을 피하는 방식입니다.
1) DP 테이블 정의
0/1 배낭 문제의 DP 테이블은 2차원 배열 dp[i][w]로 정의됩니다.
dp[i][w]는 처음i개의 물건을 고려하여, 최대 무게가w인 배낭에 담을 수 있는 최대 가치를 나타냅니다.
2) 점화식 (Recurrence Relation)
DP의 핵심은 점화식을 정의하는 것입니다. 0/1 배낭 문제의 점화식은 다음과 같습니다.
$$ dp[i][w] = \begin{cases} 0, & \text{if } i = 0 \text{ or } w = 0 \\ dp[i-1][w], & \text{if } w_i > w \\ \max(dp[i-1][w], dp[i-1][w - w_i] + v_i), & \text{otherwise} \end{cases} $$
i <mark class="highlight"><strong><u> 0또는w </u></strong></mark> 0인 경우: 물건이 없거나, 배낭의 허용 무게가 0인 경우, 담을 수 있는 가치는 0입니다.w_i > w인 경우: 현재 물건i의 무게가 배낭의 허용 무게w보다 큰 경우, 현재 물건을 담을 수 없으므로 이전 단계의 최대 가치를 그대로 가져옵니다.-
otherwise: 현재 물건i를 담을 수 있는 경우, 두 가지 선택 중 더 큰 가치를 선택합니다.dp[i-1][w]: 현재 물건을 담지 않는 경우 (이전 단계의 최대 가치)dp[i-1][w - w_i] + v_i: 현재 물건을 담는 경우, 배낭의 허용 무게에서 현재 물건의 무게를 뺀 상태에서 이전 물건들로 얻을 수 있는 최대 가치에 현재 물건의 가치를 더함.
3) 초기 조건
DP 테이블을 채우기 위한 초기 조건은 다음과 같습니다.
dp[0][w] = 0(모든w에 대해): 물건이 없는 경우, 배낭의 허용 무게와 상관없이 최대 가치는 0입니다.dp[i][0] = 0(모든i에 대해): 배낭의 허용 무게가 0인 경우, 담을 수 있는 물건이 없으므로 최대 가치는 0입니다.
4) DP 테이블 채우기
점화식과 초기 조건을 바탕으로 DP 테이블을 채워나갑니다.
i를 1부터n까지 반복합니다 (물건의 개수).w를 1부터W까지 반복합니다 (배낭의 허용 무게).- 점화식을 사용하여
dp[i][w]값을 계산합니다.
5) 코드 예시 (Python)
def knapsack_01(weights, values, capacity):
n = len(weights)
dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(1, capacity + 1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
# 예시 사용
weights = [2, 3, 4, 5]
values = [1, 2, 3, 4]
capacity = 5
max_value = knapsack_01(weights, values, capacity)
print(f"최대 가치: {max_value}")

3. 최적 부분 구조 (Optimal Substructure)
배낭 문제는 최적 부분 구조를 가집니다. 즉, 문제의 최적 해는 부분 문제의 최적 해로부터 구성될 수 있습니다. 0/1 배낭 문제의 경우, 다음과 같은 최적 부분 구조를 가집니다.
- 만약 물건
i를 배낭에 넣지 않는다면, 최대 가치는 물건1부터i-1까지의 물건들로 배낭의 무게 제한w내에서 얻을 수 있는 최대 가치와 같습니다. - 만약 물건
i를 배낭에 넣는다면, 최대 가치는 물건1부터i-1까지의 물건들로 배낭의 무게 제한w - w_i내에서 얻을 수 있는 최대 가치에 물건i의 가치를 더한 것과 같습니다.
이러한 최적 부분 구조는 DP를 적용할 수 있는 중요한 조건입니다. 문제를 작은 하위 문제로 나누어 해결하고, 하위 문제의 최적 해를 결합하여 전체 문제의 최적 해를 구할 수 있기 때문입니다.
4. 메모이제이션과 탭ulated 방식 비교
DP는 일반적으로 두 가지 방식으로 구현됩니다.
1) 메모이제이션 (Top-down)
메모이제이션은 재귀 함수를 사용하여 DP를 구현하는 방식입니다. 재귀 호출을 통해 문제를 작은 하위 문제로 나누어 해결하고, 각 하위 문제의 결과를 저장 (메모이제이션)하여 중복 계산을 피합니다.
-
장점:
- 직관적이고 코드가 간결합니다.
- 필요한 하위 문제만 계산하므로, 모든 하위 문제를 계산하는 탭ulated 방식보다 효율적일 수 있습니다 (단, 모든 하위 문제를 계산해야 하는 경우에는 탭ulated 방식이 더 효율적임).
- 단점:
- 재귀 호출로 인한 오버헤드가 발생할 수 있습니다 (함수 호출, 스택 메모리).
- 재귀 깊이가 깊어지면 스택 오버플로우가 발생할 수 있습니다.
2) 탭ulated 방식 (Bottom-up)
탭ulated 방식은 반복문을 사용하여 DP 테이블을 채워나가는 방식입니다. 작은 하위 문제부터 해결하고, 그 결과를 바탕으로 더 큰 하위 문제를 해결해 나갑니다.
-
장점:
- 재귀 호출 오버헤드가 없어 메모이제이션 방식보다 빠릅니다.
- 스택 오버플로우 발생 위험이 없습니다.
- 단점:
- 문제를 분해하는 과정이 직관적이지 않을 수 있습니다.
- 모든 하위 문제를 계산해야 하므로, 메모이제이션 방식보다 비효율적일 수 있습니다 (단, 모든 하위 문제를 계산해야 하는 경우에는 탭ulated 방식이 더 효율적임).
0/1 배낭 문제의 경우, 탭ulated 방식이 일반적으로 더 효율적입니다. 하지만 문제의 특성에 따라 메모이제이션 방식이 더 적합할 수도 있습니다.
5. 시간 복잡도 분석
0/1 배낭 문제의 DP 풀이의 시간 복잡도는 $O(nW)$입니다. 여기서 n은 물건의 개수이고, W는 배낭의 최대 허용 무게입니다. DP 테이블을 채우기 위해 n개의 물건에 대해 W개의 무게를 모두 확인해야 하기 때문입니다.
공간 복잡도는 $O(nW)$입니다. n x W 크기의 DP 테이블을 사용하기 때문입니다. 공간 복잡도를 최적화하기 위해, 이전 행의 정보만 저장하는 방식으로 공간을 줄일 수 있습니다 (예: 1차원 배열 사용).
6. 응용 및 활용 사례
0/1 배낭 문제는 다양한 분야에서 활용될 수 있습니다.
- 자원 할당 문제: 제한된 자원 (예: 예산, 시간)을 가지고 가장 높은 효용을 얻기 위해, 어떤 프로젝트나 활동에 자원을 할당할지 결정하는 문제.
- 포트폴리오 최적화: 주어진 예산 내에서 다양한 자산 (주식, 채권 등)을 선택하여 투자 포트폴리오를 구성하고, 가장 높은 수익을 얻는 조합을 찾는 문제.
- 물류 및 창고 관리: 창고에 보관할 상품을 선택하거나, 트럭에 적재할 화물을 선택하여 수송 효율을 극대화하는 문제.
- 게임 개발: 게임 내 아이템의 가치와 무게를 고려하여, 캐릭터가 소지할 수 있는 아이템의 최대 가치를 결정하는 문제.
7. 주의사항 및 트러블슈팅
- 메모리 제한:
n과W의 값이 매우 큰 경우, DP 테이블의 크기가 커져 메모리 제한에 걸릴 수 있습니다. 이 경우, 공간 복잡도를 줄이기 위한 최적화 기법 (예: 1차원 배열 사용)을 고려해야 합니다. - 정수 오버플로우: 가치 (
v_i)의 값이 큰 경우, 계산 과정에서 정수 오버플로우가 발생할 수 있습니다.long long과 같은 더 큰 자료형을 사용하거나, 가치를 적절하게 스케일링하여 오버플로우를 방지해야 합니다. - 입력 데이터 검증: 입력 데이터 (무게, 가치, 배낭의 허용 무게)가 유효한 범위 내에 있는지 확인해야 합니다. 음수 값은 처리할 수 없으므로, 음수 값이 입력될 경우 예외 처리를 해야 합니다.
- 최적 해 복원: DP 테이블을 채운 후, 어떤 물건들이 선택되었는지 (최적 해)를 역추적해야 할 수 있습니다. 이를 위해서는 DP 테이블을 채우는 과정에서 각 셀의 값을 계산할 때, 어떤 물건을 선택했는지에 대한 정보를 별도로 저장해야 합니다.
8. 결론
배낭 문제는 동적 프로그래밍을 효과적으로 적용할 수 있는 대표적인 문제 중 하나입니다. 0/1 배낭 문제의 DP 풀이를 이해하고, 점화식, 초기 조건, DP 테이블 채우는 과정을 숙지하면, 다양한 변형 문제에 적용할 수 있습니다. 최적 부분 구조와 메모이제이션/탭ulated 방식에 대한 이해는 문제 해결 능력 향상에 도움이 될 것입니다.
비슷한 글 추천
5-2. DP: 메모이제이션
메모이제이션 기법을 이용한 DP 구현, 탑다운 방식, 시간 복잡도 개선을 다룹니다.
5-1. 동적 프로그래밍 (DP) 소개
DP의 개념, 분할 정복과의 차이점, 적용 조건, 접근 방식을 소개합니다.
2-2. 프로세스 스케줄링 소개
프로세스 스케줄링의 목적, 종류, 평가 지표를 소개합니다. 다양한 스케줄링 알고리즘의 기초를 다룹니다.
3-4. 오차 역전파 (Backpropagation): 딥러닝 학습의 핵심 원리
오차 역전파 알고리즘의 원리를 수학적으로 설명하고, 다층 퍼셉트론 학습에 어떻게 적용되는지 보여줍니다.
Comments (0)
No comments yet. Be the first to comment!
Please to write a comment.