動態規劃系列之六01揹包問題

金色旭光發表於2021-01-27

揹包問題是動態規劃最具有代表性的問題。問題是這樣的:

問題

法外狂徒張三是一個探險家,有一次巧合之下進入到一個有寶藏的洞穴裡。這個洞穴有很多個不重複的寶貝,同時每個寶貝的重量也不一樣。具體來說有:
A 重 2 價值為 2
B 重 3 價值為 6
C 重 4 價值為 4
D 重 4 價值為 5
E 重 1 價值為 3
現在張三就只有一個揹包,這個揹包承重為10,張三想知道如何裝才能帶走價值最大的寶藏?

原理

在這個問題裡其實有兩個變數,第一:物品 ABCDE,是一種變數;第二:揹包的承重為另個變數。所以首先假設一種最極端的狀態:物品只有0個,揹包的重量也為0。這就是邊界值

以揹包的承重量為變數,從0到10,在物品只有0個情況下,揹包的最大價值。

0 0 0 0 0 0 0 0 0 0 0 0

關於揹包的價值和物品的重量之間有一個關係,那就是揹包放物品,如果承重大於物品的重量,則放入,揹包的價值增加,相應的揹包的承重減少。如果承重小於物品,則放不進去,揹包承重不變,價值不變。那麼到底這個物品放不放呢?主要取決於該物品放了揹包的價值和物品不放揹包的價值哪個大?(放物品的價值可不一定大於不放,因為這個相對於全域性來說的)

  • 假設變數 j 為揹包的容量,i表示第i個物品,weight[i]表示i物品的重量, value[i]表示i物品的價值,bag[i][j]表示在放入第i個物品時,揹包容量為j時的揹包價值。那麼會有一個表示式用於選擇:

  • 如果揹包容量j小於物品i的重量,放不進物品,揹包的價值等於之前物品放入的價值。所以:bag[i][j] = bag[i-1][j]
    如果揹包的容量j大於物品i的重量,能夠放入物品,那麼要不要放則取決於揹包放入物品時價值大還是不放入價值大所以: bag[i][j] = max(bag[i-1][j-weight[i]] + value[i] , bag[i-1][j])

為什麼是前一行?
因為前一行是到達當前重量時揹包內物品的最大價值,如果選擇不放當前物品,說明揹包容量不夠,那麼現在的最大價值只能是前一行的值。同理,選擇放當前物品,那麼也應該在前一行的基礎上加上當前物品的價值。

不放入任何物品時,揹包承重從0到10

當物品為0時,揹包的承重從0到10變化,其價值一直不變,始終都是0

放入物品A,揹包承重從0到10


放入物品A時,只有一個物品,所以邏輯很簡單。當揹包重量能夠裝下A的重量2時,揹包就放入A,價值就變成2。後面無論揹包承重如何變化,價值都不變。

放入物品B,揹包承重從0到10

當放入物品B時,其實是在揹包放入A的前提下進行的。

下面來一點點算出來,為什麼放入物品B時,重量變化是這樣。

i=2,j=2, j還不能放入物品B,所以bag=bag[i-1][j]bag[1][2]=2
i=2,j=3, j剛好能放入物品B,所以bag= max(bag[i-1][j-weight[i]] + value[i] , bag[i-1][j]),即bag = max(bag[1][0]+6,bag[1][3])==>bag = max(6,2)。所以bag[1][3] = 6 。放入物品B價值大
i=2,j=4, j能放入物品B,所以同上bag = max(bag[1][1]+6,bag[1][4]) ==> bag = max(6,2)。所以bag[1][4] = 6。放入物品B價值大
i=2,j=5, j能放入物品B,所以bag = max(bag[1][2]+6,bag[1][5]) ==> bag = max(8,2)。所以bag[1][5] = 8。放入物品B價值大。在揹包重量j=5時,能夠先放入B,然後再放入A。
i=2,j=6, 同上
i=2,j=7, 同上
i=2,j=8, 同上
i=2,j=9, 同上
i=2,j=10, 同上

放入物品C,揹包承重從0到10

放入物品C時,其實是在揹包中放入A和B的假設前提下進行的。

按照正常的規律去算。其中需要主要的是j=4、j=5時。
i=4,j=4 ,揹包能夠放入物品C。bag = max(bag[4-1][4-4]+4,bag[4-1][4]) ==> bag = max(4,6)。所以bag[4][4] = 6。這裡不再是放入物品C揹包價值大,而是不放入時價值更大。
i=4,j=5 ,揹包能夠放入物品C。bag = max(bag[4-1][5-4]+4,bag[4-1][5]) ==> bag = max(4,6)。所以bag[4][5] = 6。同樣,這裡不放入物品C揹包價值更大。
i=4,j=7 , 揹包能夠放入物品C。bag = max(bag[4-1][7-4]+4,bag[4-1][7]) ==> bag = max(6+4,8)。所以bag[4][7] = 10。這裡放入物品C揹包價值更大。

放入物品D,揹包承重從0到10

放入物品D時,是在假設放入ABC的基礎上進行。

需要注意的是當i=4,j=5時
i=4,j=5,揹包能夠放入物品D,bag = max(bag[4-1][5-5]+4,bag[4-1][5]) ==> bag = max(4,8)。所以bag[4][5] = 8。同樣,這裡不放入物品D揹包價值大。

放入物品E,揹包承重從0到10

放入物品E時,是在放入ABCD的前提下進行的。

最後一個放入物品E。
i=5,j=1,揹包剛好能夠放入物品E。這是bag = max(bag[5-1][1-1]+3,bag[5-1][1]) ==> bag = max(3,0)。所以bag[5][1] = 3。這是放入物品E揹包價值大

結果

最終獲得放入ABCDE五種物品的結果,也就是bag[5] 這一行的價值。最後當bag的承重為10時,最大的價值為16。所以16就是我們要求的價值。

程式碼實現

揹包最大價值


weight = [0,2,3,4,4,1]
value = [0,2,6,4,5,3]
weight_most=10

bag = [[0 for i in range(weight_most+1)] for j in range(len(weight))]

for i in range(1,len(weight)):
    for j in range(1,weight_most+1):
        if j >= weight[i]:
            bag[i][j] = max(bag[i-1][j-weight[i]]+value[i],bag[i-1][j])
        else:
            bag[i][j] = bag[i-1][j]

print(bag[-1][-1])

找到揹包裡的物品

weight = [2,3,4,4,1]
value = [2,6,4,5,3]
weight_most=10

dp = [0] * (weight_most+1)

for i in range(len(weight)):
    for j in range(weight_most,weight[i]-1,-1):
        dp[j] = max(dp[j-weight[i]]+value[i], dp[j])

print(dp)

01揹包問題優化

01揹包的時間複雜度已經無法優化,但是在空間複雜度上還存在優化的可能。
觀察以上二維陣列推導的過程,可以發現一個規律:當前這一行的的資料只和上一層資料有關。進一步觀察,可以發現是和上一層資料的前面的列有關

所以能否將二維陣列轉化為一維陣列呢?答案是可以的。使用一維陣列時,當前陣列儲存的是上一次的結果,那麼下一次的結果就直接寫入陣列就能完成資料的查詢和寫入。
但是需要從後往前遍歷和填值。因為後面的值依賴於前面的值,如果前面的值改變了,後面的依賴就不準確。

weight = [0,2,3,4,4,1]
value = [0,2,6,4,5,3]

weight_most=10

# 使用一維陣列完成記錄
bag = [0 for i in range(weight_most+1)]
for i in range(1,len(weight)):
    # 從後向前遍歷,因為後面的值依賴前面的值,從後向前不會破壞前面的值,就不會破壞後面的依賴
    for j in range(weight_most,0,-1):
        if j >= weight[i]:
            bag[j] = max(bag[j-weight[i]]+value[i],bag[j])    
print(bag[-1])

貪心演算法

貪心演算法(又稱貪婪演算法)是指,在對問題求解)時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的是在某種意義上的區域性最優解。

動態規劃和貪心演算法都是一種遞推演算法 即均由區域性最優解來推導全域性最優解

過程
動態規劃演算法將每個子問題求解一次,將其解儲存在一個表格中,需要時進行呼叫。

特徵
刻畫一個最優解的結構特徵。
遞迴的定義最優解的值。

計算最優解的值,有自頂向下和自底向上的方法,通常採用自底向上的方法。

DP思想:

1、把一個大的問題分解成一個一個的子問題。
2、如果得到了這些子問題的解,然後經過一定的處理,就可以得到原問題的解。
3、如果這些子問題與原問題有著結構相同,即小問題還可以繼續的分解。
4、這樣一直把大的問題一直分下去,問題的規模不斷地減小,直到子問題小到不能再小,最終會得到最小子問題。
5、最小子問題的解顯而易見,這樣遞推回去,就可以得到原問題的解。

與貪心法的區別:不是由上一步的最優解直接推導下一步的最優解,所以需要記錄上一步的所有解

貪心演算法:

每一步都做出當時看起來最佳的選擇,也就是說,它總是做出區域性最優的選擇。
貪心演算法的設計步驟:

  • 對其作出一個選擇後,只剩下一個子問題需要求解。
  • 證明做出貪心選擇後,原問題總是存在最優解,即貪心選擇總是安全的。
  • 剩餘子問題的最優解與貪心選擇組合即可得到原問題的最優解。

與動態規劃的區別:貪心演算法中,作出的每步貪心決策都無法改變,由上一步的最優解推導下一步的最優解,所以上一部之前的最優解則不作保留。

能使用動態規劃演算法的條件:

如果一個問題被劃分各個階段之後,階段I中的狀態只能由階段I-1中的狀態通過狀態轉移方程得來,與其它狀態沒有關係,特別是與未發生的狀態沒有關係,那麼這個問題就是“無後效性”的,可以用動態規劃演算法求解

相關文章