揹包問題描述
有n個物品,它們有各自的體積和價值,現有給定容量的揹包,如何讓揹包裏裝入的物品具有最大的價值總和?
類似的問題非常多:
比如每個任務都有時間和價值,我們有一定的時間,現在在有限的的時間裏完成最大價值的任務,如何安排?
任務 | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
所得收益 | 7 | 9 | 5 | 12 | 14 | 6 | 12 |
需要時間 | 3 | 4 | 2 | 6 | 7 | 3 | 5 |
下面就以這個問題來進行分析。
分析
如果簡單的用 收益/時間來表示收益率,其實非常容易解決這個問題,然而任務是無法分解的。不能做三分之一就停止。
我們還是用揹包問題求解。
揹包問題的核心是定義目標和找到狀態轉移公式。
求解
定義目標:求最大收益。
v[i][j] 來表示最大收益,背景:j是揹包容量(時間),前i個物品的組合;
這個是關鍵定義。
然後我們找到相鄰兩步的關係(注意我們做任何事情都需要極度關注相鄰狀態,比如馬爾科夫鏈)
v[i][j] 和上一個i-1個物品組合有何關係?
i物品太大,j放不下,那麼 v[i][j]=v[i-1][j]
i物品可以放,那麼 max(v[i-1][j], v[i-1][j-s[i]] + v[i])就是最優的。
這一步是第二個關鍵,我們分解一下。
- 放得下,那麼放的話,則 v[i-1][j-s[i]]+v[i] 這就是放完的最優狀態
- 放得下,但是不放, 收益其實不變,還是上一個狀態的v[i-1][j]
那麼再反過來思考一下:
v[i][j]是最優的
v[i-1][j-si]是最優的,空間變小後,收益增加v[i], 那麼必須比較一下,到底是放進去帶來的總體更好,還是不放留給後面的更好?
方法一: 遞歸
def value(n, space):
'''
knapsack遞歸解法
選擇第n個item的時候,到底是選擇做還是不做;選擇不做,空間不變,收益不變;
如果選擇做,那麼space要減掉,收益增加v(n)
:param n:
:param space:
:return:
'''
if n==0:
return 0
if item[n][1] >space:
return value(n-1,space)
return max(value(n-1,space),
value(n-1,space-item[n][1])+item[n][0])
方法二:遞歸記憶
方法三:直接數組
數組的方法也放在這裏,總體代碼如下,請忽略格式。
import numpy as np
item = {}
item[1] = [7,3]
item[2] = [9, 4]
item[3] = [5, 2]
item[4] = [12,6]
item[5] = [14, 7]
item[6] = [6, 3]
item[7] = [12, 5]
v = np.zeros([8,16])
def value(n, space):
'''
knapsack遞歸解法
選擇第n個item的時候,到底是選擇做還是不做;選擇不做,空間不變,收益不變;
如果選擇做,那麼space要減掉,收益增加v(n)
:param n:
:param space:
:return:
'''
if n==0:
return 0
if item[n][1] >space:
return value(n-1,space)
return max(value(n-1,space), value(n-1,space-item[n][1])+item[n][0])
def value_dp_solution(n,s):
'''
從初始化到狀態轉移,到最終解。
定義問題:value[i][j]代表面對任務i的時候,空間是j的最優收益結果。
那麼狀態轉移則是:不選擇i,直接上一個狀態;
選擇i,那麼value(i-1, space-w(i))+v(i), value(i-1,space)的最大值!
仔細思考這個狀態轉移方法。
:return:
'''
if n==0:
return 0
if v[n][s]!=0:
return v[n][s]
if item[n][1] > s:
return value(n-1,s)
result = max(value_dp_solution(n-1,s), value_dp_solution(n-1,s-item[n][1])+item[n][0])
v[n][s] = result
return result
def find_solution(a,b):
if a<1:
return
if b<=0:
return
if v[a][b]!=v[a-1][b]: #選擇了a元素
print("choose:",a)
find_solution(a-1,b-item[a][1])
else:
print("nochoose:",a)
find_solution(a-1,b)
def dp_array():
'''
問題的核心在哪裏:每次求解目標值,都需要上一步的i的值。所以v[i][0:max]先求出即可,這是最關鍵的步驟。
:return:
'''
v = np.zeros([8,16])
for i in range(1,8):
for j in range(1,16):
if j<item[i][1]:
v[i][j] = v[i-1][j]
else:
v[i][j] = max(v[i - 1][j], v[i - 1][j - item[i][1]] + item[i][0]);
print(i, v)
print(v)
# for i in range(0,8):
# print(i,value(i,15))
value_dp_solution(7,15)
print(v)
find_solution(7,15)
dp_array()
上述問題的答案
總的收益:34
選擇ABEF
總結
- 核心是定義目標函數
- 找到遞歸和轉移公式
- 利用數組和記憶更快
參考文獻
https://www.cs.cmu.edu/~avrim/451f09/lectures/lect1001.pdf