0/1 背包問題

什麼是 0/1 背包?

0/1 背包問題是 DP 最經典的問題:給定 N 個物品(每個有重量和價值),選擇一個子集放入容量 W 的背包中,使總價值最大化。每個物品只能取 0 次或 1 次

| 性質 | 意義 | |------|------| | 狀態 | dp[i][w] = 用前 i 個物品、重量 ≤ w 的最大價值 | | 遞迴 | dp[i][w] = max(跳過, 選取) = max(dp[i-1][w], dp[i-1][w-wi] + vi) | | 初始 | dp[0][w] = 0, dp[i][0] = 0 | | 時間 | O(N × W) | | 空間 | O(N × W) → 可優化到 O(W) |

實作

def knapsack_01(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        wi, vi = weights[i-1], values[i-1]
        for w in range(1, capacity + 1):
            if wi <= w:
                dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi)
            else:
                dp[i][w] = dp[i-1][w]
    
    # 回溯選取的物品
    selected = []
    w = capacity
    for i in range(n, 0, -1):
        if dp[i][w] != dp[i-1][w]:
            selected.append(i-1)
            w -= weights[i-1]
    
    return dp[n][capacity], selected

# 範例
weights = [2, 5, 4, 3, 6]
values = [6, 9, 5, 7, 8]
capacity = 10

max_val, items = knapsack_01(weights, values, capacity)
print(f"\n最大價值: {max_val}")
print(f"選取物品: {items}")
total_w = sum(weights[i] for i in items)
print(f"總重量: {total_w}/{capacity}")

# 輸出 DP 表格
print(f"\nDP 表格:")
print(f"      " + " ".join(f"{w:>3}" for w in range(capacity + 1)))
for i in range(n + 1):
    row = " ".join(f"{dp[i][w]:>3}" for w in range(capacity + 1))
    print(f"i={i}: [{row}]")

空間優化版

def knapsack_opt(weights, values, capacity):
    """O(W) 空間,O(N×W) 時間"""
    dp = [0] * (capacity + 1)
    for i in range(len(weights)):
        wi, vi = weights[i], values[i]
        for w in range(capacity, wi - 1, -1):  # 反序遍歷是關鍵!
            dp[w] = max(dp[w], dp[w-wi] + vi)
    return dp[capacity]

print(f"背包(O(W)空間): {knapsack_opt(weights, values, capacity)}")

總結

| 面向 | 詳情 | |------|------| | 狀態定義 | dp[i][w] = i 個物品、重量 ≤ w 的最大價值 | | 遞迴關係 | dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi) | | 初始條件 | dp[0][] = 0, dp[][0] = 0 | | 時間複雜度 | O(N×W) | | 空間複雜度 | O(N×W) → 優化後 O(W) | | 回溯 | 從 dp[n][W] 逆向尋找選取物品 |

常見問題與解決方案

| 問題 | 原因 | 解法 | |------|------|------| | 結果不如預期 | 參數設定不當 | 檢查預設值與邊界條件 | | 執行速度慢 | 演算法效率低 | 考慮使用更高效的資料結構 | | 記憶體不足 | 資料量過大 | 使用批次處理或串流方式 | | 難以除錯 | 缺乏日誌 | 加入詳細的 log 輸出 |



0/1 背包:動態規劃的經典應用

0/1 背包問題(每個物品只能選或不選)是動態規劃最經典的應用之一。理解它就能理解 DP 的核心——狀態定義和狀態轉移。

DP 解背包的關鍵

狀態:dp[i][w] = 前 i 個物品中,在容量 w 的限制下能達到的最大價值

轉移:dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i])
                ↑ 不選第 i 個    ↑ 選第 i 個(前提:裝得下)

時間複雜度

| 方法 | 時間 | 空間 | 適合 | |:----|:----|:----|:----| | 二維 DP | O(nW) | O(nW) | 教學用、容易理解 | | 一維 DP(滾動陣列) | O(nW) | O(W) | 實戰用、省記憶體 |

一維陣列最佳化

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [0] * (capacity + 1)
    
    for i in range(n):
        # 從右到左遍歷,避免重複使用同一物品
        for w in range(capacity, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    
    return dp[capacity]

下一章預告:最長共同子序列 LCS

背包問題是最佳化型的 DP。下一章的 LCS 是字串比對型的 DP——找出兩個字串之間最長的共同部分,應用在拼寫檢查、DNA 比對、版本差異分析。

會員專屬免費教學

本章節為註冊會員專屬的免費開放內容!請先登入或註冊會員,即可立即解鎖閱讀。

立即登入 / 註冊