揹包問題(0-1揹包、完全揹包、多重揹包)詳解
揹包問題
一個揹包總容量為V, 現在有N個物品, 第i個物品容量為weight[i], 價值為value[i], 現在往揹包裡面裝東西, 怎樣裝才能使揹包內物品總價值最大.主要分為3類:
1. 0-1揹包, 每個物品只能取0個,或者1個.
2. 完全揹包, 每個物品可以取無限次.
3. 多重揹包, 每種物品都有個數限制, 第i個物品最多可以為num[i]個.
求解思路
利用動態規劃(dynamic programming)求最優值的方法,當前狀態的最優值可以轉化成上一個狀態的最優值,與上一個狀態轉移到當前狀態代價的組合求最值。f[i]表示當前狀態的最優值,f[i-1]表示上一個狀態的最優值,s(i-1, i)表示從狀態i-1轉移帶狀態i的代價。則: f[i] = max{f[j], f[j]+s(i-1, i)}
具體問題分類
揹包問題可以根據物品個數的限制,有多種情況0-1揹包,完全揹包,多重揹包。
0-1揹包問題
0-1揹包表示每個物品只有取和不取的狀態,即只能取0個或1個。
用子問題定義狀態:即f[i][j]表示前i間物品恰放入一個容器為j的揹包可以獲得的最大價值。狀態轉移方程為:
f[i][j] = max{f[i-1][j], f[i-1][j-weight[i]]+value[i]}
python程式碼實現如下:
def ZeroOnePack(N, V, weight, value):
"""
0-1 揹包問題(每個物品只能取0次, 或者1次)
:param N: 物品個數, 如 N=5
:param V: 揹包總容量, 如V=15
:param weight: 每個物品的容量陣列表示, 如weight=[5,4,7,2,6]
:param value: 每個物品的價值陣列表示, 如weight=[12,3,10,3,6]
:return: 返回最大的總價值
"""
# 初始化f[N+1][V+1]為0, f[i][j]表示前i件物品恰放入一個容量為j的揹包可以獲得的最大價值
f = [[0 for col in range(V+1)] for row in range(N+1)]
for i in range(1, N+1):
for j in range(1, V+1):
if j<weight[i-1]: # 總容量j小於物品i的容量時,直接不考慮物品i
f[i][j] = f[i-1][j]
else:
# 注意由於weight、value陣列下標從0開始,第i個物品的容量為weight[i-1],價值為value[i-1]
f[i][j] = max(f[i-1][j], f[i-1][j-weight[i-1]]+value[i-1]) # 狀態方程
max_value = f[N][V]
return max_value
上面程式碼中例子的f[N+1][V+1]每一步計算如下表,其中橫向表示物品編號,縱向表示揹包容量:
N\V | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 12 | 12 | 12 | 12 | 12 | 12 | 12 | 12 | 12 | 12 | 12 |
2 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 15 | 15 | 15 | 15 | 15 | 15 |
3 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 15 | 15 | 22 | 22 | 22 | 22 |
4 | 0 | 0 | 3 | 3 | 3 | 12 | 12 | 15 | 15 | 15 | 15 | 18 | 22 | 22 | 25 | 25 |
5 | 0 | 0 | 3 | 3 | 3 | 12 | 12 | 15 | 15 | 15 | 15 | 18 | 22 | 22 | 25 | 25 |
所以程式碼中示例取的最大價值是選取1,3,4號物品,
總容量=5+7+2<=15滿足條件,總價值=12+10+3=25.
完全揹包問題
完全揹包表示每個物品可以取無限次,只要加起來總容量不超過V就可以。
同樣可以用f[i][j]表示前i間物品恰放入一個容器為j的揹包可以獲得的最大價值。則其狀態轉移方程為:
f[i][j] = max{f[i-1][j-k*weight[i]]+k*value[i]} ,其中(0<=k<=j/weight[i])
python程式碼實現如下:
def CompletePack(N, V, weight, value):
"""
完全揹包問題(每個物品可以取無限次)
:param N: 物品個數, 如 N=5
:param V: 揹包總容量, 如V=15
:param weight: 每個物品的容量陣列表示, 如weight=[5,4,7,2,6]
:param value: 每個物品的價值陣列表示, 如value=[12,3,10,3,6]
:return: 返回最大的總價值
"""
# 初始化f[N+1][V+1]為0,f[i][j]表示前i件物品恰放入一個容量為j的揹包可以獲得的最大價值
f = [[0 for col in range(V + 1)] for row in range(N + 1)]
for i in range(1, N+1):
for j in range(1, V+1):
# 注意由於weight、value陣列下標從0開始,第i個物品的容量為weight[i-1],價值為value[i-1]
# V/weight[i-1]表示物品i最多可以取多少次
f[i][j] = f[i - 1][j] # 初始取k=0為最大,下面的迴圈是把取了k個物品i能獲得的最大價值賦值給f[i][j]
for k in range(j/weight[i-1]+1):
if f[i][j] < f[i-1][j-k*weight[i-1]]+k*value[i-1]:
f[i][j] = f[i-1][j-k*weight[i-1]]+k*value[i-1] # 狀態方程
# 上面的f[i][j]也可以通過下面一行程式碼求得
# f[i][j] = max([f[i-1][j-k*weight[i-1]]+k*value[i-1] for k in range(j/weight[i-1]+1)])
max_value = f[N][V]
return max_value
上面程式碼中例子的f[N+1][V+1]每一步計算如下表,其中橫向表示物品編號,縱向表示揹包容量:
N\V | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 12 | 12 | 12 | 12 | 12 | 24 | 24 | 24 | 24 | 24 | 36 |
2 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 24 | 24 | 24 | 24 | 27 | 36 |
3 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 24 | 24 | 24 | 24 | 27 | 36 |
4 | 0 | 0 | 3 | 3 | 6 | 12 | 12 | 15 | 15 | 18 | 24 | 24 | 27 | 27 | 30 | 36 |
5 | 0 | 0 | 3 | 3 | 6 | 12 | 12 | 15 | 15 | 18 | 24 | 24 | 27 | 27 | 30 | 36 |
所以程式碼中示例取的最大價值是選取1號物品3個,
總容量=5x3<=15滿足條件,總價值=12x3=36.
多重揹包問題
多重揹包是每個物品有不同的個數限制,如第i個物品個數為num[i]。
同樣可以用f[i][j]表示前i間物品恰放入一個容器為j的揹包可以獲得的最大價值,且每個物品數量不超多num[i]。則其狀態轉移方程為:
f[i][j] = max{f[i-1][j-k*weight[i]]+k*value[i]} ,其中(0<=k<=min{j/weight[i], num[i]})
def MultiplePack(N, V, weight, value, num):
"""
多重揹包問題(每個物品都有次數限制)
:param N: 物品個數, 如 N=5
:param V: 揹包總容量, 如V=15
:param weight: 每個物品的容量陣列表示, 如weight=[5,4,7,2,6]
:param value: 每個物品的價值陣列表示, 如value=[12,3,10,3,6]
:param num: 每個物品的個數限制,如num=[2,4,1,5,3]
:return: 返回最大的總價值
"""
# 初始化f[N+1][V+1]為0,f[i][j]表示前i件物品恰放入一個容量為j的揹包可以獲得的最大價值
f = [[0 for col in range(V + 1)] for row in range(N + 1)]
for i in range(1, N+1):
for j in range(1, V+1):
# 對於物品i最多能取的次數是j/weight[i-1]與num[i-1]中較小者
max_num_i = min(j/weight[i-1], num[i-1])
f[i][j] = f[i - 1][j] # 初始取k=0為最大,下面的迴圈是把取了k個物品i能獲得的最大價值賦值給f[i][j]
for k in range(max_num_i+1):
if f[i][j] < f[i-1][j-k*weight[i-1]]+k*value[i-1]:
f[i][j] = f[i-1][j-k*weight[i-1]]+k*value[i-1] # 狀態方程
# 上面的f[i][j]也可以通過下面一行程式碼求得
# f[i][j] = max([f[i-1][j-k*weight[i-1]]+k*value[i-1] for k in range(max_num_i+1)])
max_value = f[N][V]
return max_value
上面程式碼中例子的f[N+1][V+1]每一步計算如下表,其中橫向表示物品編號,縱向表示揹包容量:
N\V | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 12 | 12 | 12 | 12 | 12 | 24 | 24 | 24 | 24 | 24 | 24 |
2 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 24 | 24 | 24 | 24 | 27 | 27 |
3 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 24 | 24 | 24 | 24 | 27 | 27 |
4 | 0 | 0 | 3 | 3 | 6 | 12 | 12 | 15 | 15 | 18 | 24 | 24 | 27 | 27 | 30 | 30 |
5 | 0 | 0 | 3 | 3 | 6 | 12 | 12 | 15 | 15 | 18 | 24 | 24 | 27 | 27 | 30 | 30 |
所以程式碼中示例取的最大價值是選取1號物品2個,4號物品選2個,
總容量=5x2+2x2<=15滿足條件,總價值=12x2+3x2=30.