deck:: Logseq/coding tip

- ### **배낭 문제 (Knapsack Problem) 개요**
	- **정의:** 담을 수 있는 최대 무게가 정해진 배낭에, 각각 무게와 가치를 가진 물건들을 선택해서 담을 때 {{c1 **가치의 합이 최대**}}가 되도록 하는 조합을 찾는 문제!
	  id:: 69c7a933-0563-49ed-ba0a-9a0d16d2fe95
	  #+BEGIN_EXTRA
	  
	  **응용**
	  - 주어진 예산(무게 제한) 내에서 최대 만족도(가치)를 얻는 선택 문제
	  - 특정 시간 내에 가장 높은 점수를 얻을 수 있는 업무/행동 스케줄링
	  
	  #+END_EXTRA
	- **종류**
		- id:: 69c7aa4b-2786-4234-ab56-b64506c9ae9e
		  1. **Fractional Knapsack (분할 가능 배낭 문제):** 물건을 쪼갤 수 있는 경우 -> {{c1 그리디(Greedy) 알고리즘 :: 사용 알고리즘?}}으로 풀이
		  
		  2. **0-1 Knapsack (0-1 배낭 문제):** 물건을 쪼갤 수 없는 경우 (넣거나/안 넣거나). -> {{c1 DP(동적 계획법) :: 사용 알고리즘?}}로 풀이
-
- ### **0-1 배낭 문제의 DP 풀이 (2차원 배열)**
	- **상태 정의:** dp[i][w] = {{c1 i번째 물건까지 고려했을 때, 배낭의 임시 무게 한도가 w일 때 얻을 수 있는 최대 가치 :: 말로 풀어서 설명할 것!}}
	  id:: 69c7a933-f266-4fb2-b2a1-d384d24c47b4
	- **점화식 (가장 중요!):**
		- ▶︎ 현재 물건의 무게가 w보다 커서 배낭에 못 넣을 때: dp[i][w] = {{c1 dp[i-1][w]}}                                                ▶︎ 넣을 수 있을 때 : dp[i][w] = {{c1 max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i])}} 
		  id:: 69c7a933-c467-408c-856b-2b7ad4197e51
		  extra:: 즉, 이전 물건까지의 최적해를 그대로 가져오거나, 현재 물건을 넣기 위해 배낭 공간을 비웠을 때의 최적해에 현재 가치를 더한 것 중 더 큰 값을 선택한다.
	- **시간/공간 복잡도:** {{c1 $O(N * W)$}} (N: 물건 개수, W: 배낭의 최대 무게)
	  id:: 69c7a933-5096-444e-9c15-8ca2b16936d9
	- **코드**
		- **매개변수는 다음과 같다.**
		  id:: 69c7af20-ef44-4e3e-a7b8-b4e109536554
		  
		  W : 배낭의 최대 용량
		  N : 물건의 갯수
		  weight : i번째 물건의 무게를 나열한 배열(N+1 크기의 배열로 인덱스 1번부터 N번까지 값이 들어가있음)
		  value : i번째 물건의 가치를 나열한 배열(N+1 크기의 배열로 인덱스 1번부터 N번까지 값이 들어가있음)
		  
		  위 매개변수를 입력으로 받는 2차원 배열을 이용해 가장 배낭문제를 푸는 파이썬 함수(knapsack_2d)를 작성하라 #card
			- ```python
			  def knapsack_2d(W, N, weight, value):
			      	# dp[n+1][W+1] 크기의 2차원 배열을 0으로 초기화
			          dp = [[0 for _ in range(W + 1)] for _ in range(N + 1)]
			  
			          # 물건을 하나씩 늘려가며 확인 (i: 물건 인덱스, w: 현재 배낭 용량)
			          for i in range(1, N + 1):
			              for w in range(1, W + 1):
			                  # 1. 현재 물건의 무게가 배낭 용량보다 작거나 같아서 '넣을 수 있는' 경우
			                  if weight[i] <= w:
			                      # (넣는 경우의 가치) vs (안 넣는 경우의 가치) 중 최댓값
			                      dp[i][w] = max(value[i] + dp[i-1][w - weight[i-1]], dp[i-1][w])
			                  # 2. 물건이 너무 무거워서 '못 넣는' 경우
			                  else:
			                      # 이전 물건까지의 최적해를 그대로 가져옴
			                      dp[i][w] = dp[i-1][w]
			  
			          return dp[N][W]
			  ```
-
- ### **공간 복잡도 최적화: 1차원 배열 풀이**
	- **원리:** 2차원 점화식을 보면 dp[i][w]는 항상 {{c1 dp[i-1]}}행의 값만 참조한다. 따라서 2차원 테이블 전체를 유지할 필요 없이 1차원 배열만으로 dp배열을 정의해서 이전 값을 참조해서 새롭게 dp 배열을 갱신하는 식으로 공간 사용을 줄일 수 있다.
	  id:: 69c7a933-dfc1-4ea7-98ff-6535ca789c08
	  이때 dp[w]의 상태 정의는 {{c1 현재 고려하는 물건에 대해 임시 무게 한도가 w일 때 얻을 수 있는 최대 가치}} 이다.
	- **주의할 점 (순회 방향):**
		- 1차원 배열로 최적화할 때는 내부 반복문(무게 w)을 반드시 {{c1 뒤에서 앞으로(W부터 1까지 역순으로)}} 순회해야 한다.
		  id:: 69c7a933-d658-4856-b75d-77f068956848
		  #+BEGIN_EXTRA
		  
		  **왜 역순으로 해야 할까?**
		  만약 앞에서 뒤로(1부터 W까지) 순회하면, 현재 물건(i)을 이미 넣어서 갱신된 dp 값을 같은 i번째 물건 계산에 또 참조해 버리는 불상사가 생긴다. (물건을 여러 번 중복해서 넣는 꼴이 됨)
		  뒤에서부터 순회하면 항상 아직 갱신되지 않은 이전 상태(dp[i-1])의 값을 안전하게 참조할 수 있다.
		  만약 물건을 중복해서 담을 수 있는 경우라면 앞에서 부터 순회해야 한다.
		  #+END_EXTRA
	- **코드**
		- **매개변수는 다음과 같다.**
		  id:: a5f6207b-6ce6-4c84-a5e7-ee96f915eda6
		  
		  W : 배낭의 최대 용량
		  N : 물건의 갯수
		  weight : i번째 물건의 무게를 나열한 배열(N+1 크기의 배열로 인덱스 1번부터 N번까지 값이 들어가있음)
		  value : i번째 물건의 가치를 나열한 배열(N+1 크기의 배열로 인덱스 1번부터 N번까지 값이 들어가있음)
		  
		  위 매개변수를 입력으로 받는 1차원 배열을 이용해 배낭문제를 푸는 파이썬 함수(knapsack_1d)를 작성하라 #card
			- ```python
			  def knapsack_1d(W, N, weight, value):
			          # dp[W+1] 크기의 1차원 배열을 0으로 초기화
			          dp = [0 for _ in range(W + 1)]
			  
			          for i in range(1, N + 1):
			              # 핵심!! 내부 반복문은 반드시 뒤에서부터(역순으로) 순회해야 한다!
			              # W부터 현재 물건의 무게(wt[i])까지만 순회 (그보다 작으면 어차피 못 넣으니까)
			              for w in range(W, weight[i] - 1, -1) :
			                  # 1차원 배열 덮어쓰기: (현재 물건을 넣은 값) vs (기존에 저장된 값)
			                  dp[w] = max(value[i] + dp[w - weight[i]], dp[w])
			  
			          return dp[W]
			  ```