0/1 背包問題

0/1 背包問題的重點整理。

https://www.hello-algo.com/zh-hant/chapter_dynamic_programming/knapsack_problem/#__tabbed_4_6

1. 問題定義

  • 給定 (Given):

    • \(n\) 個物品 (items)。

    • 每個物品 \(i\) 有一個重量 \(w_i\) 和一個價值 \(v_i\)。

    • 一個背包,其最大可承受重量 (容量) 為 \(W\)。

  • 限制 (Constraint):

    • 對於每個物品,你只能選擇 拿 (1) 或 不拿 (0)。

    • 不能只拿物品的一部分(這與「部分背包問題」Fractional Knapsack Problem 不同)。

  • 目標 (Goal):

    • 求解在總重量不超過 \(W\) 的前提下,能放入背包的最大總價值。

筆記重點:

Greedy (貪婪法) 無法保證得到 0/1 背包問題的最佳解。

此問題具有最佳子結構 (Optimal Substructure),適合使用 DP 求解。

2. Recursive Form (遞迴關係式)

我們使用一個二維陣列(表格)\(C[i, k]\) 來定義子問題:

\(C[i, k]\) = 考慮前 \(i\) 個物品 (item 1 到 \(i\)),在背包容量上限為 \(k\) 時,所能得到的最大總價值。

我們的目標是求出 \(C[n, W]\)。

遞迴推導:

當我們考慮第 \(i\) 個物品(其重量為 \(w_i\),價值為 \(v_i\))時,有兩種選擇:

  1. 不放第 \(i\) 個物品:

    • 這可能是因為不想放,或是因為放不下 (\(k < w_i\))。

    • 最大價值會等於「只考慮前 \(i-1\) 個物品,且容量為 \(k\)」時的最大價值。

    • 即 \(C[i-1, k]\)。

  2. 放第 \(i\) 個物品:

    • 這必須在 \(k \ge w_i\) (容量足夠) 的前提下才能發生。

    • 總價值會等於「第 \(i\) 個物品的價值 \(v_i\)」加上「考慮前 \(i-1\) 個物品,且容量剩下 \(k - w_i\)」的最大價值。

    • 即 \(v_i + C[i-1, k - w_i]\)。

關係式總結:

\(C[i, k]\) 就是取上述兩種情況中,價值較大的一個。

\[C[i, k] = \begin{cases} 0 & \text{if } i = 0 \text{ or } k = 0 \\ C[i-1, k] & \text{if } k < w_i \text{ (放不下第 i 個物品)} \\ \max(C[i-1, k], \quad v_i + C[i-1, k - w_i]) & \text{if } k \ge w_i \text{ (可選擇放或不放)} \end{cases}\]

3. 演算法 (Algorithm) - Bottom-up DP

我們可以使用「由下而上」(Bottom-up) 的方式,填滿 \(C[i, k]\) 這個表格來求解。

程式碼片段

// 演算法:0/1 Knapsack
// 輸入: n (物品數量), W (總容量), v (價值陣列), w (重量陣列)
// 輸出: 最大價值

// 1. 建立一個 (n+1) x (W+1) 的表格 C
create table C[0..n, 0..W]

// 2. 初始化 Base Case (第 0 列)
// 當沒有物品(i=0)時,任何容量 k 的價值都是 0
for k <- 0 to W:
    C[0, k] <- 0

// 3. 迭代所有物品 i (從 1 到 n)
for i <- 1 to n:
    // 初始化 Base Case (第 0 行)
    // 當容量為 0 (k=0)時,任何物品 i 都放不進,價值為 0
    C[i, 0] <- 0

    // 4. 迭代所有容量 k (從 1 到 W)
    for k <- 1 to W:
        // 取得第 i 個物品的價值 v_i 和重量 w_i
        
        // 情況 1: 當前容量 k < 物品 i 的重量
        if k < w_i:
            // 放不下,價值等於「不放」的情況
            C[i, k] <- C[i-1, k]
        
        // 情況 2: 當前容量 k >= 物品 i 的重量
        else:
            // 比較「不放」和「放」哪個價值高
            C[i, k] <- max(
                            C[i-1, k],                  // 不放第 i 個物品
                            v_i + C[i-1, k - w_i]       // 放第 i 個物品
                        )

// 5. 最終答案
// 考慮了全部 n 個物品,且總容量為 W 時的最大價值
return C[n, W]

4. 複雜度分析 (Complexity Analysis)

  • 時間複雜度 (Time Complexity): \(O(nW)\)

    • 原因: 來自兩個巢狀迴圈 (nested loop)。外層迴圈跑 \(n\) 次 (for \(i\)),內層迴圈跑 \(W\) 次 (for \(k\))。
  • 空間複雜度 (Space Complexity): \(O(nW)\)

    • 原因: 需要一個 \(C[0..n, 0..W]\) 的二維表格來儲存子問題的解,表格大小為 \((n+1) \times (W+1)\),也就是額外花費的記憶體。

填表過程

直觀的說

1. 裝得下嗎? (\(k < w_i\))

  • 不行: 答案 = 抄樓上 (\(C[i-1, k]\))

2. 裝得下。 (\(k \ge w_i\))

  • 答案 = 比較下面兩者,選大的:

    • 不拿: 樓上 (\(C[i-1, k]\))

    • 拿: \(\text{斜上} + \text{新價值}\) (\(C[i-1, k - w_i] + v_i\))

「斜上」 就是指:往上一列,再往左 \(w_i\) 格。

物品 1:\(i=1, w_1=2, v_1=6\)

\(C[1,k]=\max{C[0,k],6+C[0,k-2]}\)

結果列:

\(C[1,k]\) 0 1 2 3 4 5
值 0 0 6 6 6 6

物品 2:\(i=2, w_2=3, v_2=10\)

\(C[2,k]=\max{C[1,k],;10+C[1,k-3]}\)

結果列:

\(C[2,k]\) 0 1 2 3 4 5
值 0 0 6 10 10 16

物品 3:\(i=3, w_3=4, v_3=12\)

\(C[3,k]=\max{C[2,k],12+C[2,k-4]}\)

最終列:

\(C[3,k]\) 0 1 2 3 4 5
值 0 0 6 10 12 16

完整表格

\(C[i,k]\) 0 1 2 3 4 5
\(i=0\) 0 0 0 0 0 0
\(i=1\) 0 0 6 6 6 6
\(i=2\) 0 0 6 10 10 16
\(i=3\) 0 0 6 10 12 16

答案:\(C[n,W]=C[3,5]=16\)

回溯解集合

從 \(C[3,5]=16\) 往回:

  1. \(C[3,5]=16= C[2,5]\) ⇒ 不選物品 3。移到 \((2,5)\)

  2. \(C[2,5]=16 \ne C[1,5]=6\) ⇒ 選物品 2。\(k\leftarrow 5-3=2\),移到 \((1,2)\)

  3. \(C[1,2]=6 \ne C[0,2]=0\) ⇒ 選物品 1。\(k\leftarrow 2-2=0\),移到 \((0,0)\) 結束

選擇:\({1,2}\)
總重量:\(2+3=5\)
總價值:\(6+10=16\)