【C++】競程筆記(背包問題)
【C++】競程筆記(背包問題)
題目範例參考:NTUCPC Guide,此筆記僅為個人學習用途。
最後更新時間:
什麼是背包問題
背包問題(Knapsack Problem)是一種組合最佳化的 NP-Complete 問題,有著多種變形,其中最基礎的是「0/1 背包問題」。
在 DP 裡面,背包問題是最經典且常見的題型之一。
01 背包問題
Problem Source:https://oj.ntucpc.org/problems/801
所謂 0 1 就是物品可拿或不拿。
題目敘述:
有 $N$ 個物品編號 $1 \sim N$ ,第 $i$ 個物品的重量和價值分別是 $w_i$ 和 $v_i$ 。學姐打算從這 $N$ 個物品選其中一些帶走,但她只有大小為 $W$ 的背包,也就是說她選擇的物品總重不能超過 $W$ 。請問背包能容納的物品的總價值最大是多少?
如果直接定義 $dp[i]$ 是前 $i$ 個物品的最大總價值,是不實際的,因為還要考慮到 $W$ 重量的因素。
因此用到二維 DP 觀念:$dp[i][j]$ 為前 $i$ 個物品且背包當前容量限制為 $j$ 時,能獲得的最大價值。
對於第 $i$ 個物品(重量 $w[i]$,價值 $v[i]$),當考慮是否要放入背包(當前容量 $j$)時,只有兩種選擇:
- 不拿第 $i$ 個物品:最大價值 = 前 $i - 1$ 個物品且容量為 $j$ 的最大總價值。$$dp[i][j] = dp[i-1][j]$$
- 拿第 $i$ 個物品:前提是背包要夠裝 ($j \ge w[i]$),如果拿了,會得到 $v[i]$ 的價值,但背包容量會減少 $w[i]$。剩下的容量 $j - w[i]$ 就要去查詢「前 $i-1$ 個物品」能湊出的最大價值。$$dp[i][j] = dp[i-1][j - w[i]] + v[i]$$
由於要求最大價值,因此 $$dp[i][j] = \max(dp[i-1][j], \quad dp[i-1][j - w[i]] + v[i])$$
最後在 base case 定義上,設定為全部都是 0。
範例程式碼:
1 |
|
0/1 背包問題變形:鐵棒問題
此為經典的子集合加總問題(Subset Sum Problem),屬於 0/1 背包問題的變形。
Problem Source:https://oj.ntucpc.org/problems/802
- 定義狀態: $dp[i][j]$ 是一個布林值,其意義為從前 $i$ 條鐵棒中選出若干條,使其長度總和恰好為 $j$,則為 true;否則為 false。
- 定義轉移式:
對於第 $i$ 條鐵棒(其長度為 $l_i$),有兩種選擇:
- 不選這條鐵棒:如果前 $i-1$ 條鐵棒能湊出長度 $j$,那前 $i$ 條當然也能。$$dp[i-1][j]$$
- 選這條鐵棒:前提是目標長度 $j$ 必須大於等於鐵棒長度 $l_i$,如果選了,問題就變成「前 $i-1$ 條鐵棒能否湊出長度 $j - l_i$」。$$dp[i-1][j - l_i] \ , (j \ge l_i)$$
完整轉移式:$$dp[i][j] = dp[i-1][j] \lor dp[i-1][j - l_i]$$
當中 $\lor$ 表示 OR,其中一個可以湊出長度 $j$ 就行。
- 定義初始狀態:沒鐵棒時($i=0$):
- 湊出長度為 0 是可能的(一條都不選):$dp[0][0] = \text{true}$
- 湊出任何大於 0 的長度是不可能的:$dp[0][j] = \text{false}$ (for $j > 0$)
範例程式碼:
1 |
|
0/1 背包問題變形:高棕櫚農場
Problem Source:https://tioj.sprout.tw/problems/143
為什麼是背包問題?
在有限的飽足感容量( $M$ )限制下,選擇若干個高棕櫚(物品),使得總滿足感(價值)最大化。
- 定義狀態:令 $dp[j]$ 表示當飽足感上限為 $j$ 時,能夠獲得的最大滿足感。(原問題可用二維 dp 做,但會超時,因此在這邊將狀態壓縮成一維)
- 定義轉移式:
對於每個高棕櫚 $i$(其飽足感為 $A_i$,滿足感為 $B_i$)有兩種選擇:
- 不吃:最大滿足感保持不變,即 $dp[j]$。
- 吃:需要消耗 $A_i$ 的飽足感,因此目前的滿足感會變成「剩餘容量 $j - A_i$ 時的最大滿足感」加上「當前高棕櫚的滿足感 $B_i$」,即 $dp[j - A_i] + B_i$。
則取兩種選擇的最大值即為轉移式:$$dp[j] = \max(dp[j], \quad dp[j - A_i] + B_i)$$
- 定義初始狀態:滿足感全 0($dp[0 \cdots M] = 0$)。
範例程式碼:
由於每個物品只能選一次,在用一維 dp 計算時,內層迴圈須由大到小(從 $M$ 遞減到 $A_i$)遍歷,這樣可確保計算 $dp[j]$ 時所參考的 $dp[j - A_i]$ 是來自「上一個物品」的狀態,而不是「當前物品」已經被選過之後的狀態。
1 |
|
0/1 背包問題變形:高棕櫚農場2
Problem Source:https://tioj.sprout.tw/problems/144
在原本的架構上,這題又給你加一個限制: $K$ ,吃掉高棕櫚的上限。
解題邏輯跟上題差不多,不管物品 $i$ 的維度,只關心 $M, K$ 的維度即可,此為滾動 DP 優化空間的做法。
- 定義狀態:令 $dp[j][k]$ 為當前飽足感為 $j$,且吃了 $k$ 個高棕櫚時,所能獲得的最大滿足感。
- 定義轉移式:
對於每個高棕櫚 $i$(其飽足感為 $A_i$,滿足感為 $B_i$)有兩種選擇:
- 不吃:狀態維持不變,即 $$dp[j][k]$$
- 吃:前提是當前飽足感 $j \ge A_i$ 且數量 $k \ge 1$。此時滿足感為由「飽足感 $j-A_i$ 且數量 $k-1$」的狀態加上當前的滿足感 $B_i$,即 $$dp[j-A_i][k-1] + B_i$$
完整轉移式: $$dp[j][k] = \max(dp[j][k], \quad dp[j - w][k - 1] + v)$$
- 定義初始狀態:跟上題一樣,dp 全 0。
範例程式碼:
1 |
|
背包問題的另種形式:無限背包問題(Unbounded Knapsack Problem)
無限背包問題又稱為完全背包問題,與 0/1 背包問題不同的是:完全背包問題的每種物品的數量是無限的,只要背包裝得下,同一種物品可選 1 個、2 個,甚至無限個。
接下來請看問題。
Problem Source:https://oj.ntucpc.org/problems/825
:::info
Description
現在有 $N$ 個物品,第 $i$ 個物品的重量為 $w_i$ ,價值為 $v_i$ 。每個物品都有無限多個。你有一個重量限制為 $W$的背包,你希望可以在不超過這個背包重量限制的前提下,盡可能塞入價值總和最高的物品。請問你可以塞入最高的物品總價值是多少?
:::
- 定義狀態: $dp[j]$ 為當背包重量總和為 j 時所能裝入物品的最大價值總和。所求的解為 $dp[W]$ ,背包最大重量限制。
- 定義轉移式:
- 不選:價值不變,仍為 $dp[j]$ 。
- 選:選後背包剩餘可裝重量為 $j - w_i$ 。此時的價值就是「容量為 $j - w_i$ 時的最大價值」加上「當前物品的價值 $v_i$」。
最終得到完整轉移式:$$dp[j] = \max(dp[j], \quad dp[j - w_i] + v_i)$$
- 定義初始狀態:全都是 0。
範例程式碼:
可發現在 0/1 背包問題中,內層迴圈往往都是以「逆序」遍歷,也就是 $W \rightarrow w_i$ 的走訪。
但到了無限背包問題,就會相反過來變成「正序」遍歷( $w_i \rightarrow W$ )
差別在於 0/1 背包問題每個物品都只能取一次,而無限背包可以無限取,也就是每件物品可以重複取的意思,因而需要將無限背包改成正序遍歷。
- 正序(無限背包):計算 $dp[j]$ 時, $dp[j-w_i]$ 是新狀態,可能已包含當前物品。
- 逆序(0/1 背包):計算 $dp[j]$ 時, $dp[j-w_i]$ 是舊狀態,不包含當前物品。
1 |
|



