揹包問題及其優化-python實現

什麼是揹包問題

揹包問題hi是組合優化的NP完全問題。問題的描述是:有一組物品,每種物品都有自己的重量和價值,現在挑選一些放到有重量限制的揹包裏,如何挑選物品才能使得揹包裏的物品總價值最大。揹包問題廣泛出現在商業、組合數學等場合。
針對物品的數量種類以及放置的規則,可以分爲0-1揹包問題,完全揹包問題和多重揹包問題等。

0-1揹包問題

0-1揹包問題是最基礎的揹包問題。其特點是,每件物品僅有一件,可以選擇放或者不放入揹包。

完全揹包問題

完全揹包問題是在0-1揹包問題基礎上擴展。不同於0-1問題的每件物品僅僅可以選擇一個放入書包,在完全揹包問題裏,每種物品都有無限件可以用。

多重問題

多重問題是介於0-1揹包和完全揹包之間的。即每種物品有有限件,問題就可以描述爲將哪些物品裝入揹包可使這些物品的體積總和不超過揹包容量,且價值總和最大。

揹包問題的求解

暴力搜索方法求解

揹包問題的求解方法很多,最直觀最容易想到的就是列舉法,即暴力搜索方法。把物品的每種組合可能列出來,然後計算總價值找到最佳的組合。這種方法在物品種類和數量較少時候可以使用,但是隨着物品種類數量的增大將,計算複雜的將急劇上升。一種改進方法是採用遺傳算法對物品的是否放入揹包進行基因編輯,然後在通過遺傳交叉變異,求解結果。這種方法具有遺傳算法的本身缺點,其中一個就是不能保證最優解。

貪婪算法

貪婪算法根據物品的價值或者價值比(價值除以重量),或者某一種合理的貪婪指標,按照次序貪婪地放入揹包,貪婪算法無法得到最優解,甚至有時候性能極差。

動態規劃解決揹包問題

動態規劃的基本思路是把大問題拆成小問題,通過尋找大問題與小問題的遞推關係,解決一個個小問題,最終達到解決原問題的效果。在動態規劃,遞推步驟具有記憶性,通過填表法把已解決的子問題記錄下來,在新問題需要用到的子問題可以直接提取,避免重複計算,從而節約時間。在問題滿足最優性後,用動態規劃的核心就是填表,當填表結束,最優解也就找到了。我們先來看看動態規劃解決0-1揹包問題。

0-1揹包問題描述和建模

設有n個物品,每個物品的重量記爲wiw_{i},價值記爲viv_{i},揹包的容量爲CC。設xix_{i}表示第i個物品是否放入揹包,1表示放入,0表示不放入。那麼問題的優化目標,使得價值最大化的數學表示形式爲
i=1nvixi\sum_{i=1}^{n}v_{i}x_{i}
這裏有兩個約束,一個是物品不能超過揹包的最大容量,xix_{i}是二值。即
i=1nwixi<=C\sum_{i=1}^{n}w_{i}x_{i}<=C以及
xi0,1,1<=i<=nx_{i} \in {0,1}, 1<=i<=n
最優性原理是動態規劃的基礎,最優性原理是指多階段決策過程中,無論初始狀態和初始決策如何,對於前面決策所造成的某一個狀態而言,其後各個階段的決策序列必須構成最優策略。
V(i,wb)V(i,wb)爲當前揹包的剩餘容量爲wb,前i個物品最佳組合對應的價值
根據最優性原理,那麼面對當前物品(即第i個物品)有兩種可能性

  1. 包的容量比該物品小,即物品大過包的容量,放不下了。此時揹包裏的價值和前i-1個的價值是一樣的,即V(i,wb)=V(i1,wb)V(i,wb)=V(i-1,wb)
  2. 包的剩餘容量比該物品大,即還有空間放置該物品。但是裝下該物品是否能達到當前最優價值呢?這就需要看裝與不裝之間的比較了。不裝,V(i,wb)=V(i1,wb)V(i,wb)=V(i-1,wb)。裝了,V(i,wb)=V(i1,wbwi)+viV(i,wb)=V(i-1,wb-w_{i})+v_{i},即揹包容量減少了,但是價值增大了。此時需要取兩種情況的最大值。
    由此可以得出遞推關係式:
  • wb<wiwb<w_{i}時,不裝,V(i,wb)=V(i1,wb)V(i,wb)=V(i-1,wb)
  • wb>=wiwb>=w_{i}時,V(i,wb)=max{V(i1,wb),V(i1,wbwi)+vi}V(i,wb)=max\{V(i-1,wb), V(i-1,wb-w_{i})+v_{i} \}

0-1揹包示例

下面我們舉例說明,設揹包的容量爲10,有5個物品可供裝包,其質量和體積如下表。
在這裏插入圖片描述
根據遞推公式進行逐行填表,初始化表V(i,o) = 0,V(0,j) = 0,表的列維度從0到n,行維度從0到capacity。填表如下:
在這裏插入圖片描述
下面我們來分析一下這個表格:
表格的第一列0~5的數字,其中1 ~5是物品的編號。第一行0 ~ 10是揹包的容量。除此以外的數字是物品組合的價值。下面一行一行的來分析。

  1. 第0行元素都是零,即什麼都不放的時候,揹包價值爲0
  2. 第1行,當面對物品1時,物品1的價值爲6,體積爲2。此時第1行的第0列和第1列書包容量爲0和1,不足以裝下物品1,因此V(i,wb)=V(i1,wb)V(i,wb)=V(i-1,wb)被執行,即價值爲0。第2列,容量爲2,剛好裝下物品1。如果裝物品1,那麼書包價值爲6。不裝則價值認爲0。因此選擇裝,即執行V(i,wb)=max{V(i1,wb),V(i1,wbwi)+vi}V(i,wb)=max\{V(i-1,wb), V(i-1,wb-w_{i})+v_{i} \}。第3列,容量爲3,也能裝下物品1。執行V(i,wb)=max{V(i1,wb),V(i1,wbwi)+vi}V(i,wb)=max\{V(i-1,wb), V(i-1,wb-w_{i})+v_{i} \},價值仍然是6。同理第4、5、6 、。。。10列也是一樣。
  3. 第2行,當面對物品2,物品2的價值爲3,體積爲2。此時第1行的第0列和第1列書包容量爲0和1,不足以裝下物品1和物品2。第2列,容量爲2。此時我們需要求V(2,2)V(2,2),即面對物品2,容量爲2的情況下如何放物品。物品2的容量等於揹包容量,即wb>=wiwb>=w_{i}成立,則V(i,wb)=max{V(i1,wb),V(i1,wbwi)+vi}V(i,wb)=max\{V(i-1,wb), V(i-1,wb-w_{i})+v_{i} \}將被執行,帶入相應的數值,得到
    V(2,2)=max{V(1,2),V(1,22)+3}V(2,2)=max\{V(1,2), V(1,2-2)+3 \}。由於V(1,2)=6V(1,2)=6V(1,22)+3=0+3V(1,2-2)+3 =0+3,則V(2,2)=6V(2,2)=6,在此框裏填入6。當第3行時情況和第2行一樣。當第4列時,V(2,4)=max{V(1,4),V(1,42)+3}V(2,4)=max\{V(1,4), V(1,4-2)+3 \},由於V(1,4)=6V(1,4)=6V(1,42)+3=6+3V(1,4-2)+3 =6+3,所以填入9。後面的列也是如此。
  4. 第3行,當面對物品3,物品3的價值爲5,體積爲6。第1行的第0列和第1列和前面情況一樣,wb<wiwb<w_{i}時,填0。第2列,第3、4 、5列,wb<wiwb<w_{i}時,同樣執行V(i,wb)=V(i1,wb)V(i,wb)=V(i-1,wb)。直到第6列,wb>=wiwb>=w_{i}V(3,6)=max{V(2,6),V(2,66)+5}V(3,6)=max\{V(2,6), V(2,6-6)+5 \},取6和5的最大值。第7列也如此。第8列,V(3,8)=max{V(2,8),V(2,86)+5}V(3,8)=max\{V(2,8), V(2,8-6)+5 \},取9和6+5之間的最大值。第10列,V(3,10)=max{V(2,10),V(2,106)+5}V(3,10)=max\{V(2,10), V(2,10-6)+5 \},取9和9+5之間的最大值。
  5. 同理直到填滿所有表格。

python代碼實現

上述的推理過程用python來實現,代碼如下:

# -*- coding: utf-8 -*-
# @Time    : 2019/12/3 15:22
# @Author  : HelloWorld!
# @FileName: bag.py
# @Software: PyCharm
# @Operating System: Windows 10
# @Python.version: 3.6

def zeroOneBig(num,capacity,weighList,valueList):
    valueExcel=[[0 for j in range(capacity+1)] for i in range(num+1)]
    print(valueExcel)
    for i in range(1,num+1):
        for j in range(1,capacity+1):
            valueExcel[i][j]=valueExcel[i-1][j]
            if j>=weighList[i-1] and valueExcel[i][j]<(valueExcel[i-1][j-weighList[i-1]]+valueList[i-1]):
                valueExcel[i][j]=valueExcel[i-1][j-weighList[i-1]]+valueList[i-1]
            for row in range(num+1):

                print(valueExcel[row])
            print('----------------------')
    return valueExcel
weight_list=[2,2,6,5,4]
value_list=[6,3,5,4,6]
valueExcel=zeroOneBig(5,10,weight_list,value_list)

輸出的最後結果如下

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6]
[0, 0, 6, 6, 9, 9, 9, 9, 9, 9, 9]
[0, 0, 6, 6, 9, 9, 9, 9, 11, 11, 14]
[0, 0, 6, 6, 9, 9, 9, 10, 11, 13, 14]
[0, 0, 6, 6, 9, 9, 12, 12, 15, 15, 15]
----------------------

由此可見,該問題的價值最大組合爲15。該方法在時間複雜度上不能再優化了,但是在空間複雜度上還可以優化,即可以把上述的列表表示成一組。
代碼如下:

def zeroOneOpt(num,capacity,weighList,valueList):
    valueRes=[0 for j in range(capacity+1)]

    for i in range(1,num+1):
        for j in range(capacity,0,-1):
            if j >weighList[i-1]:
                valueRes[j]=max(valueRes[j-weighList[i-1]]+valueList[i-1],valueRes[j])
        print('***********')
        print(valueRes)
    return valueRes
zeroOneOpt(5,10,weight_list,value_list)

結果如下:

***********
[0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6]
***********
[0, 0, 0, 6, 6, 9, 9, 9, 9, 9, 9]
***********
[0, 0, 0, 6, 6, 9, 9, 9, 9, 11, 11]
***********
[0, 0, 0, 6, 6, 9, 9, 9, 10, 11, 13]
***********
[0, 0, 0, 6, 6, 9, 9, 12, 12, 15, 15]

找到最佳組合

儘管已經找到最佳的價值總和是15,但是我們還不知道是哪幾個物品的組合構成這個最優解。根據填表原理,V(i,wb)=V(i1,wb)V(i,wb)=V(i-1,wb)時,說明沒有放置物品i,V(i,wb)!=V(i1,wb)V(i,wb) !=V(i-1,wb)時,說明裝了物品i,該物品是最優解的部分,此時由於$V(i,wb)= V(i-1,wb-w_{i})+v_{i} ,回到V(i-1,wb-w_{i})$進一步尋找。重複直到遍歷完所有物品。下面我們來分析表格,看看哪些個物品是最佳組合。

在這裏插入圖片描述
我們從最後一列,最後一行開始看。V(5,10)!=V(4,10)V(5,10)!=V(4,10),因此物品5(體積爲4,價值爲6)是最佳組合之一。根據$V(i,wb)= V(i-1,wb-w_{i})+v_{i} 跳躍到V(4,10-4)。由於V(4,6)=V(3,6)=94V(3,6),說明物品4沒有被放入揹包,不是最佳組合之一。下一步跳躍到V(3,6),V(3,6)=V(2,6)=93,說明物品3也不是最佳組合之一。進入V(2,6),,V(2,6)!=V(1,6)$,說明物品2是最佳組合。同理可得物品1也是最佳組合。即物品5,2,1是最佳組合。
實現代碼如下:

def showRes(num,capacity,wightList,valueExcel):
    indexRes=[]
    j=capacity
    for i in range(num,0,-1):
        if valueExcel[i][j]!=valueExcel[i-1][j]:
            indexRes.append(i)
            j-=wightList[i-1]
    return indexRes
print(showRes(5,10,weight_list,valueExcel))

結果爲[5, 2, 1]

完全揹包問題示例

根據0-1揹包問題的遞推公式,通過轉換變化,得到完全揹包問題的遞推公式。
假設第i個物品放入揹包的數量爲k,而當前揹包的容量爲wb,那麼k必須滿足
0<=k<=wb//wi0<=k<=wb//w_{i}
因此

  • 0<=k<=wb//wi0<=k<=wb//w_{i}時,V(i,wb)=max{V(i1,wb),V(i1,wbkwi)+kvi}V(i,wb)=max\{V(i-1,wb), V(i-1,wb-k*w_{i})+k*v_{i} \}
    python代碼如下:
def compKnap(num,capacity,weightList,valueList):
    valueExcel = [[0 for j in range(capacity + 1)] for i in range(num + 1)]
    for i in range(1, num + 1):
        for j in range(1, capacity + 1):
            for k in range((j // weightList[i - 1]) + 1):
                if valueExcel[i][j] < (valueExcel[i - 1][j - k*weightList[i - 1]] + k*valueList[i - 1]):
                    valueExcel[i][j] = (valueExcel[i - 1][j - k * weightList[i - 1]] + k*valueList[i - 1])
    return valueExcel
print(compKnap(5,16,[5,4,7,2,6],[12,3,10,3,6]))

回溯法:

https://zhuanlan.zhihu.com/p/51015629
回溯法是一個既帶有系統性又帶有跳躍性的的搜索算法。它在包含問題的所有解的解空間樹中,按照深度優先的策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任一結點時,總是先判斷該結點是否肯定不包含問題的解。如果肯定不包含,則跳過對以該結點爲根的子樹的系統搜索,逐層向其祖先結點回溯。否則,進入該子樹,繼續按深度優先的策略進行搜索。回溯法在用來求問題的所有解時,要回溯到根,且根結點的所有子樹都已被搜索遍才結束。而回溯法在用來求問題的任一解時,只要搜索到問題的一個解就可以結束。這種以深度優先的方式系統地搜索問題的解的算法稱爲回溯法,它適用於解一些組合數較大的問題。

0—1揹包問題是一個子集選取問題,適合於用子集樹表示0—1揹包問題的解空間。在搜索解空間樹是,只要其左兒子節點是一個可行結點,搜索就進入左子樹,在右子樹中有可能包含最優解是才進入右子樹搜索。否則減去右子樹。

爲了便於計算上界,可先將物品依其單位重量價值從大到小排序,此後只要順序考察各物品即可。在實現時,由MaxBoundary函數計算當前結點處的上界。它是類Knap的私有成員。Knap的其他成員記錄瞭解空間樹種的節點信息,以減少函數參數的傳遞以及遞歸調用時所需要的棧空間。在解空間樹的當前擴展結點處,僅當要進入右子樹時才計算上界函數MaxBoundary,以判斷是否可以將右子樹減去。進入左子樹時不需要計算上界,因爲其上界與父結點的上界相同。

當使用回溯法時,對於有N個物品的0-1揹包問題,其解空間由長度爲n的0-1向量組成,樹的深度等於物品的個數。
如N=3時候,問題可以理解爲:
在這裏插入圖片描述
例如n=3, C=30, w={16, 15, 15}, v={45, 25, 25}。
在這裏插入圖片描述

參考文獻:
https://blog.csdn.net/ggdhs/article/details/90648890

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章