動態規劃之0,1揹包問題

公眾號程式設計師學長發表於2021-08-13

   我們在上一篇文章初識動態規劃已經對動態規劃的演算法思想有了一定的瞭解,今天我們再來通過一個經典問題:0,1揹包問題,從更深層次的角度來認識一下動態規劃演算法。建議先看上一篇文章,再來看這篇。

   首先,我們來看一下什麼是0,1揹包問題。   

   問題描述:給定 n 件物品,物品的重量分別為w1、w2、w3....,現需要挑選物品放入揹包中,假定揹包能承受的最大重量為V,問應該如何選擇裝入揹包中的物品,使得裝入揹包中物品的的重量最大?

  首先,我們最直觀的想法就是,窮舉所有可能的裝法,然後從中選出滿足條件的最大值。我們可以使用回溯演算法來實現。如下所示:

class BagQ:
     #假設物品的重量都大於0
     #揹包的最大承重也大於0
     maxW=0
     weight=[2,2,8,3,5,3]  #物品重量
     n=6   #物品個數
     w=10  #揹包的最大承重
     def getMax(self,i,cw):
          if cw==self.w or i==self.n: #揹包裝滿或者物品被考察完了
               if cw>self.maxW:
                    self.maxW=cw
               return
          self.getMax(i+1,cw) #第i個物品不放入揹包
​
          #考察放入第i個物品後,會不會超過揹包的容量
          if cw+self.weight[i]<=self.w:
               self.getMax(i+1,cw+self.weight[i]) #選擇裝第i個物品
​
bag=BagQ()
bag.getMax(0,0)
print(bag.maxW)

  我們通過程式碼可以看到回溯演算法的時間複雜度較高,是指數級別的。那有什麼方法可以降低時間複雜度嗎?我們最好的方式就是把遞迴呼叫樹畫出來,來找找規律。遞迴呼叫樹如下所示:

 

      遞迴樹的每個節點表示一種狀態,用(i,w)來表示。比如f(1,2)表示第一個物品放入揹包,此時揹包的重量為2,下一步f(2,2)表示第二個物品不放入揹包,此時揹包的重量不變。而f(2,4)表示第二個物品放入揹包,此時揹包的重量為4。

      從上圖我們可以發現,會有重複的子問題出現,比如f(2,2)被計算了兩次,那我們該如何避免重複計算呢?

      我們可以這麼來看,我們把整個求解階段分為n個階段,每個階段去決策一個物品是否放入揹包。每個物品決策完之後,對應的揹包中物品的重量會有多種可能,也就是多種狀態。

      我們來一步一步分析。

  1. 第一個物品的重量為2,我們先來決策第一個物品是否放入揹包,它有兩種可能,要麼放入,要麼不放,與之相對應的揹包的重量也有兩種可能,要麼是0,要麼是2。

  2. 第二個物品的重量為2,我們再來決策第二個物品是否放入揹包,它也有兩種可能,要麼放入,要麼不放,與之相對應的揹包的重量就不是兩種可能了,它有4種可能(我們的排列組合知識可以派上用場了)。它需要依賴於上個物品是否放入揹包,所以它是需要依賴於上一個狀態的。

       .....

      從上面的分析來看,第n個階段揹包的狀態是需要依賴於第n-1個階段的,所以我們需要把上一個階段的狀態儲存下來,才能快速的求出這個階段的狀態,因此狀態轉移矩陣就出來了。我們這裡需要定義一個二維的陣列,來記錄不同階段的狀態。如下圖所示:

     下面我們來看程式碼是如何實現的:

def bag(weight,n,w):
     status=[[0 for _ in range(w+1)] for _ in range(n)]
     status[0][0]=1
     if(weight[0]<=w):
          status[0][weight[0]]=1
     for i in range(n): #動態規劃狀態轉移
          #不把第i個物品放入揹包
          for j in range(w+1):
               if status[i-1][j] == 1:
                    status[i][j] = 1
          #把第i個物品放入揹包
          for j in range(w+1-weight[i]):
               if status[i-1][j] == 1:
                    status[i][j+weight[i]] = 1
     #輸出結果
     print(status)
     for i in range(w,-1,-1):
          if status[n-1][i]==1:
               return i
     return 0
​
weight=[2,2,8,3,5,3]
n=6
w=10
print(bag(weight,n,w))

     我們通過把問題分解為多個階段,每個階段對應一個決策。然後記錄下每一個階段可達的狀態集合(去掉重複的),然後通過當前階段的狀態集合,來推導下一個階段的狀態集合,依次前進,從而把問題解決。

       接下來,我們再來把0,1揹包問題升級一下,引入物品價值這一說。也就是針對一組不同價值、不同重量的物品,我們將物品放入揹包中,在滿足揹包最大重量的限制條件下,揹包中可裝入物品的總價值最大是多少呢?這個思路和上一個思路類似,我這裡就不在贅述。建議大家先用回溯演算法實現,然後畫出遞迴樹,最後寫出狀態轉移矩陣,再實現程式碼。我這裡直接給出程式碼。如果有問題,歡迎大家留言。

def bag(weight,value,n,w):
     status=[[-1 for _ in range(w+1)] for _ in range(n)]
     status[0][0]=0
     if(weight[0]<=w):
          status[0][weight[0]]=value[0]
     for i in range(n): #動態規劃狀態轉移
          #不把第i個物品放入揹包
          for j in range(w+1):
               if status[i-1][j] >= 0:
                    status[i][j] = status[i-1][j]
          #把第i個物品放入揹包
          for j in range(w+1-weight[i]):
               if status[i-1][j] >= 0:
                    v=status[i-1][j]+value[i]
                    if(v>status[i][j+weight[i]]):
                         status[i][j+weight[i]]=v
     #輸出結果
     print(status)
     maxV=0
     for i in range(w+1):
          if status[n-1][w]>maxV:
               maxV=status[n-1][w]
     return maxV
​
weight=[2,2,8,3,5,3]
value=[3,4,12,6,3,2]
n=6
w=10
print(bag(weight,value,n,w)) 

      經過這篇文章和上一篇文章,我們應該對動態規劃有了一個清晰的認識,我會在下一篇把問題抽象一下,看哪類問題適合動態規劃來解決,以及解決動態規劃問題的思考過程是怎麼樣的?為了不錯過,請關注公眾號。

 

相關文章