---
# System prepended metadata

title: DP背包

---

## 背包問題

---

### 背包問題是啥

背包問題是**動態規劃 (Dynamic Programming, DP)** 最經典的問題之一。它的核心思想是通過將問題拆解成子問題，慢慢一步一步解完他們。


![image](https://hackmd.io/_uploads/HJvJ3Iggeg.png)




#### 背包問題在幹嘛?
- 有一個容量為 `W` 的背包。
- 有 `n` 件物品，每件物品有兩個屬性：
  - **重量 (`weight[i]`)**
  - **價值 (`value[i]`)**
  (這裡會把`weight`和`value`當成陣列分別存每個物品的重量和價值)
- **目標**：讓放進背包的物品總重量不超過 `W`，且總價值最大化。
- **限制**：每個物品最多只能選擇一次（這就是所謂的「01 背包問題」），每個背包問題都會有些不一樣的限制，這裡先用最常見也最基礎的01背包做講解

---

### 二維 DP 背包問題 (01 背包)
![image](https://hackmd.io/_uploads/ry4H28lele.png)


#### 狀態定義
我們用一個**二維陣列 `dp[i][j]`** 來表示：
- `dp[i][j]`：表示考慮前 `i` 件物品，且背包容量為 `j` 時，所能達到的最大價值。

換句話說：
- 如果我們只考慮前 `i` 件物品，並且背包容量為 `j`，那麼可以裝進背包的最大價值是多少。

---

#### 狀態轉移方程式

![favorite-facts](https://hackmd.io/_uploads/ryAch8lllg.gif)


```cpp=
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
```

這個公式分成兩部分：
1. **不選擇第 `i` 件物品**：
   - 如果不選擇第 `i` 件物品，那麼最大價值等於「只考慮前 `i-1` 件物品，且剩餘背包容量為 `j` 時的最大價值」。
   - 對應的狀態是 `dp[i-1][j]`。

2. **選擇第 `i` 件物品**：
   - 如果選擇第 `i` 件物品，那麼最大價值等於「只考慮前 `i-1` 件物品，且背包剩餘容量為 `j - weight[i]` 時的最大價值」，再加上第 `i` 件物品的價值 `value[i]`。
   - 對應的狀態是 `dp[i-1][j-weight[i]] + value[i]`。

3. **取兩者的較大值**：
   - 我們肯定是希望最後的價值是最大的，所以取 `max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])`。

---

#### 初始條件
1. **背包容量為 0 時** (`j = 0`)：
   - 沒東西放，你當然一點價值都沒有(嗯?  所以 `dp[i][0] = 0`。

2. **沒有物品可選時** (`i = 0`)：
   - 無論背包容量多大，你沒東西拿，還是一樣一點價值都沒有(嗯?  所以 `dp[0][j] = 0`。

---


## 每次進入 `dp[i][j]` 判斷時他到底會怎麼走：

#### 假設當前 `i = 3`，`j = 5`，物品 `3` 的重量為 `3`，價值為 `4`，我們檢查 `dp[3][5]`。

#### 1. **檢查能否選擇物品 `3`**：
   - 條件：`j >= weight[3]`。
   - 如果 `j = 5` 且 `weight[3] = 3`，滿足條件，可以選擇物品。

#### 2. **計算不選擇物品 `3` 的情況**：
   - 最大價值為「只考慮前 `2` 件物品，且容量為 `5` 時的最大價值」。
   - 對應：`dp[2][5]`。

#### 3. **計算選擇物品 `3` 的情況**：
   - 假設選擇物品 `3`，需要考慮剩餘容量 `j - weight[3] = 5 - 3 = 2`。
   - 最大價值為「前 `2` 件物品在容量 `2` 時的最大價值」加上物品 `3` 的價值 `4`。
   - 對應：`dp[2][2] + value[3]`。

#### 4. **取兩者的最大值**：
   - 比較 `dp[2][5]` 和 `dp[2][2] + value[3]`，取較大值作為 `dp[3][5]`。

---

## 全過程的逐步演示：

假設有以下物品：
| 物品編號 | 重量 (`weight`) | 價值 (`value`) |
|----------|----------------|----------------|
| 1        | 2              | 3              |
| 2        | 1              | 2              |
| 3        | 3              | 4              |
| 4        | 2              | 2              |

背包容量 `W = 5`。

初始 `dp` 陣列為：
```
dp[i][j] = 0 (所有 i 和 j 都初始化為 0)
```

---

### 第 1 件物品 (`weight[1] = 2`, `value[1] = 3`)：
- 只更新容量 `j >= 2` 的情況，公式為：
  ```cpp
  dp[1][j] = max(dp[0][j], dp[0][j-2] + 3);
  ```

| 容量 (`j`) | 更新公式                 | 結果 |
|------------|--------------------------|------|
| `j = 0`    | 無法放入，`dp[1][0] = 0` |  0   |
| `j = 1`    | 無法放入，`dp[1][1] = 0` |  0   |
| `j = 2`    | `max(0, 0 + 3)`          |  3   |
| `j = 3`    | `max(0, 0 + 3)`          |  3   |
| `j = 4`    | `max(0, 0 + 3)`          |  3   |
| `j = 5`    | `max(0, 0 + 3)`          |  3   |

---

### 第 2 件物品 (`weight[2] = 1`, `value[2] = 2`)：
- 只更新容量 `j >= 1` 的情況，公式為：
  ```cpp
  dp[2][j] = max(dp[1][j], dp[1][j-1] + 2);
  ```

| 容量 (`j`) | 更新公式                 | 結果 |
|------------|--------------------------|------|
| `j = 0`    | 無法放入，`dp[2][0] = 0` |  0   |
| `j = 1`    | `max(0, 0 + 2)`          |  2   |
| `j = 2`    | `max(3, 0 + 2)`          |  3   |
| `j = 3`    | `max(3, 3 + 2)`          |  5   |
| `j = 4`    | `max(3, 3 + 2)`          |  5   |
| `j = 5`    | `max(3, 3 + 2)`          |  5   |

---

### 第 3 件物品 (`weight[3] = 3`, `value[3] = 4`)：
- 只更新容量 `j >= 3` 的情況，公式為：
  ```cpp
  dp[3][j] = max(dp[2][j], dp[2][j-3] + 4);
  ```

| 容量 (`j`) | 更新公式                 | 結果 |
|------------|--------------------------|------|
| `j = 0`    | 無法放入，`dp[3][0] = 0` |  0   |
| `j = 1`    | 無法放入，`dp[3][1] = 2` |  2   |
| `j = 2`    | 無法放入，`dp[3][2] = 3` |  3   |
| `j = 3`    | `max(5, 0 + 4)`          |  5   |
| `j = 4`    | `max(5, 2 + 4)`          |  6   |
| `j = 5`    | `max(5, 3 + 4)`          |  7   |

---

### 最終結果：
當考慮所有物品且背包容量為 `5` 時，`dp[4][5]` 的最大價值為 `7`。

---

## 所以?：
1. **每次進入 `dp[i][j]` 時，我們會檢查是否能選擇當前物品**，並分別計算選擇與不選擇的情況。
2. **狀態轉移公式的核心是取兩者的最大值**，這保證了我們逐步構建最優解。
3. **通過動態規劃，我們有效地解決了重疊子問題，並利用最優子結構性質找到整體最優解**。

#### 實現範例（完整程式碼）

假設有以下物品：
- 物品 1：重量 2，價值 3
- 物品 2：重量 1，價值 2
- 物品 3：重量 3，價值 4
- 物品 4：重量 2，價值 2

背包容量為 `W = 5`。

```cpp name=knapsack_2d.cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
    int W = 5;
    vector<int> weight = {2, 1, 3, 2};
    vector<int> value = {3, 2, 4, 2};
    int n = weight.size();
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= n; i++){
        for (int j = 0; j <= W; j++){
            if (j >= weight[i-1]){
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1]);
            } 
			else{
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    cout << "最大價值: " << dp[n][W] << '\n';

}
```

---

### 為什麼這樣做可以解決背包問題？
1. **問題拆解**：
   - 這樣解可以將原問題拆解成「是否選擇當前物品」的兩種情況，這樣可以逐步縮小問題規模，並用子問題的解構建出原問題的解。

2. **最優子結構**：
   - 每個子問題的最優解可以用來構建更大的問題的最優解。
   - 例如，`dp[i][j]` 的最優解是由 `dp[i-1][j]` 和 `dp[i-1][j-weight[i]] + value[i]` 的最優解組成的。

3. **那又為何使用dp?**：
   - 在計算過程中，許多子問題會被重複計算。
   - 這很顯然就是dp的適用情況了，用陣列把結果存下來避免過多非必要運算

---

### 壓縮到一維 DP
#### 核心邏輯
仔細思考會發現，背包用二維陣列存其實很浪費空間，陣列裡的`i`其實不太必要，我們可以嘗試刪掉後改成一維陣列，可以有效減少空間浪費。

#### 狀態定義
- **`dp[j]`**：表示背包容量為 `j` 時的最大價值。

#### 狀態轉移方程式
轉移方程式與二維 DP 相同：
```cpp=
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
```

#### 小重點：遍歷順序
- 必須從右向左更新 `dp[j]`。
- **為什麼？**
  - 如果從左向右更新，會導致當前面的結果影響到後面的計算，然後開始一步錯步步錯，跟我的人生一樣拿到WA

---

#### 一維 DP 實現範例

```cpp name=knapsack_1d.cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
    int W = 5;
    vector<int> weight = {2, 1, 3, 2};
    vector<int> value = {3, 2, 4, 2};
    int n = weight.size();
    vector<int> dp(W + 1, 0);
    for (int i = 0; i < n; i++){
        for (int j = W; j >= weight[i]; j--){
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << "最大價值: " << dp[W] << '\n';
}
```

---

### 完全背包問題 (無限次選擇物品)

#### 問題描述

每種物品可以被選擇無限次，但總重量不能超過背包容量 `W`。

---

#### 狀態轉移方程式
與 01 背包的公式類似，但是每種物品可以被多次選擇，因此需要改變遍歷順序：
```cpp=
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
```

#### 小重點：遍歷順序
- 必須從左向右更新 `dp[j]`，以確保每個物品可以被重複使用。
- **為什麼？**
  - 如果從右向左遍歷，會錯過重複選擇同一物品的機會。

---

#### 完全背包實現範例

```cpp name=knapsack_unbounded.cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
    int W = 5;
    vector<int> weight = {2, 1, 3, 2};
    vector<int> value = {3, 2, 4, 2};
    int n = weight.size();
    vector<int> dp(W + 1, 0);
    for (int i = 0; i < n; i++){
        for (int j = weight[i]; j <= W; j++){
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << "最大價值: " << dp[W] << '\n';
}
```

---

### 小結

1. **二維 DP** 是理解背包問題的基礎方法，有助於清晰地看到每層的狀態轉移。
2. **一維 DP** 是對空間的優化，適合處理大規模問題。
3. **完全背包** 是背包問題的重要變形，通過改變遍歷順序來實現無限次選擇。
