1. 程式人生 > 實用技巧 >動態規劃——分組揹包問題

動態規劃——分組揹包問題

之前簡單介紹了一下0/1揹包問題,詳情可見動態規劃——0/1揹包問題

但是後來在做華為機考的另一道題,發現這道題需要用有依賴的揹包問題來解決,接下來我為大家簡單介紹一下這道題以及如何用有依賴的揹包問題來解決。

一、題目描述

王強今天很開心,公司發給N元的年終獎。王強決定把年終獎用於購物,他把想買的物品分為兩類:主件與附件,附件是從屬於某個主件的,下表就是一些主件與附件的例子:

主件 附件
電腦 印表機,掃描器
書櫃 圖書
書桌 檯燈,文具
工作椅
如果要買歸類為附件的物品,必須先買該附件所屬的主件。每個主件可以有0個、1個或2個附件。附件不再有從屬於自己的附件。王強想買的東西很多,為了不超出預算,他把每件物品規定了一個重要度,分為5等:用整數1~5表示,第5等最重要。他還從因特網上查到了每件物品的價格(都是10元的整數倍)。他希望在不超過N元(可以等於N元)的前提下,使每件物品的價格與重要度的乘積的總和最大。 設第j件物品的價格為v[j],重要度為w[j],共選中了k件物品,編號依次為j1,j2,……,jk,則所求的總和為: v[j1]*w[j1]+v[j2]*w[j2]+…+v[jk]*w[jk]。(其中*為乘號) 請你幫助王強設計一個滿足要求的購物單。

輸入描述

輸入的第1行,為兩個正整數,用一個空格隔開:Nm

(其中N(<32000)表示總錢數,m(<60)為希望購買物品的個數。) 從第2行到第m+1行,第j行給出了編號為j-1的物品的基本資料,每行有3個非負整數vpq (其中v表示該物品的價格(v<10000),p表示該物品的重要度(1~5),q表示該物品是主件還是附件。如果q=0,表示該物品為主件,如果q>0,表示該物品為附件,q是所屬主件的編號)

輸出描述

輸出檔案只有一個正整數,為不超過總錢數的物品的價格與重要度乘積的總和的最大值(<200000)

示例輸入

1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0

示例輸出

2200

二、分組揹包問題

我們可以看到,這道題目裡面不同於0/1揹包問題,它有個主件與附件的依賴問題(買主件就必須買附件),所以這就用到我們的分組揹包問題。

分組揹包問題首先要做的就是分組,分好組之後,若組內的選擇大於一種,則只能在組內選擇其中一種,或者全部不選,然後再按照0/1揹包問題去構建二維陣列即可。

舉個簡單的例子:

假設有五件物品:1,2,3,4,5,分組情況為(1,2),(3,4),(5),且問題描述與0/1揹包問題相同。

我們就可以畫出二維陣列如下:

1 2 3 ...
1,2 B(1,1) B(1,2) B(1,3) ...
3,4 B(2,1) B(2,2) B(2,3) ...
5 B(3,1) B(3,2) B(3,3) ...

我們以計算B(2,3)為例:

在將第二組考慮進來之後,我們只能選擇要不全部不選,要不只選擇物品3,要不只選擇物品4,所以我們可以得到下面的公式:

當然,物品3或者4可選的前提條件是 j >= w[3] 或 j >= w[4]。若不滿足該條件則不加進來作比較。

三、題解

根據題目描述的主件與附件的關係,我們可以分組如下:

第一組:[ ] 空列表

第二組:[ (1, ), (1, 2 ), (1, 3 ), (1, 2, 3)] 主件1與其附件的選擇

第三組:[ ( 4,), ] 物品4

第四組:[ ( 5,), ] 物品5

故我們可以畫出二維陣列如下:

0 10 20 ... 1000
[ ]
[ (1, ), (1, 2 ), (1, 3 ), (1, 2, 3)] B(2,j)
[ ( 4,), ]
[ ( 5,), ]

同樣以B(2, j)為例,我們看一下求解公式:

四、程式碼實現

接下來我們用程式碼實現上述演算法:

首先為了構建分組,我們這裡用到了python中的chain與combinations模組

from itertools import chain,combinations

def powerset(iterable):
    # powerset([1,2,3]) ——> [(),(1,),(2,),(3,),(1,2,),(1,3),(2,3),(1,2,3)]
    s = list(iterable)
    return list(chain.from_iterable(combinations(s,r) for r in range(len(s)+1)))

記下來寫主程式碼:

初始化:

from collections import defaultdict

capacity,num = list(map(int,input().split()))
p = [0] # 價格
w = [0] # 重要程度
c = [0] # 從屬關係
d = defaultdict(list)  # d[key] 預設為[]
for i in range(num):
    price,weight,cate = list(map(int,input().split()))
    p += [price]
    w += [weight]
    c += [cate]

分組:

for i in range(1,num+1):
    if c[i] == 0:
        d[i] =[]
for i in range(1,num+1):
    if c[i] != 0:
        d[c[i]].append(i)

groups = [[0]] # 分組
for k,v in d.items():
    l = powerset(v)
    l = list(map(lambda x: x+(k,),l))
    groups.append(l)

動態規劃:

table = [[0 for column in range(int(capacity//10)+1)] for row in range(len(groups))]
for i in range(1,len(groups)):
    for j in range(1,int(capacity//10)+1):
        competition = [table[i-1][j]]
        for group in groups[i]:
            sum_price = 0
            sum_weight_price = 0
            for item in group:
                sum_price += p[item] # 把每個item價格加起來
                sum_weight_price+=p[item]*w[item]
            if j*10 >= sum_price:
                competition += [table[i-1][j-int(sum_price/10)]+sum_weight_price] #加上一個group的對應值
        table[i][j] = max(competition)
print(table[-1][-1])