1. 程式人生 > >揹包問題(0-1揹包、完全揹包、多重揹包)詳解

揹包問題(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.