演算法筆記之分組揹包(HJ16 購物單)
這個題感覺在華為機試題庫裡面算複雜的了,當然對比力扣裡某些題還算簡單。
原題連結
描述
王強決定把年終獎用於購物,他把想買的物品分為兩類:主件與附件,附件是從屬於某個主件的,下表就是一些主件與附件的例子:
主件 | 附件 |
---|---|
電腦 | 印表機,掃描器 |
書櫃 | 圖書 |
書桌 | 檯燈,文具 |
工作椅 | 無 |
如果要買歸類為附件的物品,必須先買該附件所屬的主件,且每件物品只能購買一次。
每個主件可以有 0 個、 1 個或 2 個附件。附件不再有從屬於自己的附件。
王強查到了每件物品的價格(都是 10 元的整數倍),而他只有 N 元的預算。除此之外,他給每件物品規定了一個重要度,用整數 1 ~ 5 表示。他希望在花費不超過 N 元的前提下,使自己的滿意度達到最大。
滿意度是指所購買的每件物品的價格與重要度的乘積的總和,假設設第ii件物品的價格為v[i],重要度為w[i],共選中了k件物品,編號依次為j_1,j_2,...,j_k,則滿意度為:v[j_1]w[j_1]+v[j_2]
請你幫助王強計算可獲得的最大的滿意度。
01揹包基礎
最基礎的揹包問題:有n件物品和一個最多能背重量為w 的揹包。第i件物品的重量是weight[i],得到的價值是value[i] 。每件物品只能用一次,求解將哪些物品裝入揹包裡物品價值總和最大。
dp解法的一般步驟如下:
- 定義dp[i][j]的含義:dp[i][j] 表示從下標為[0-i]的物品裡任意取,放進容量為j的揹包,價值總和最大是多少。
- 確定遞推公式:
dp[i][j] 表示從下標為[0-i]的物品裡任意取,放進容量為j的揹包,價值總和最大是多少, 所以有兩種情況推出來:- 不放物品i:由dp[i - 1][j]推出,即揹包容量為j,裡面不放物品i的最大價值,此時dp[i][j]就是dp[i - 1][j]。(其實就是當物品i的重量大於揹包j的重量時,物品i無法放進揹包中,所以被揹包內的價值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 為揹包容量為j - weight[i]的時候不放物品i的最大價值,那麼dp[i - 1][j - weight[i]] + value[i] (物品i的價值),就是揹包放物品i得到的最大價值
所以遞迴公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- dp陣列初始化
如果物品的重量和價值如下,揹包重量為4:重量 價值 物品0 1 15 物品1 3 20 物品2 4 30 - 首先從dp[i][j]的定義出發,如果揹包容量j為0的話,即dp[i][0],無論是選取哪些物品,揹包價值總和一定為0
- 當只有編號為0的物品時,當揹包容量等於或超過物品0的重量時,最大價值只能是編號為0物品的價值
所以針對上面的物品和揹包的dp[i][j],當i0或j0時初始值如下。而其他的座標因為後面可以遞推出來,值都可以初始化為0.
- 確定遍歷順序
二維陣列先遍歷揹包容量還是先遍歷物品不影響。 - 舉例推導驗證
當揹包容量為3,從編號為0和1的兩個物品裡取,最大價值會是多少?明顯的只能取物品1,此時最大價值是20.
- 關鍵程式碼
''' weight, value分別為物品的重量和價值的一維陣列 ''' rows, cols = len(weight), bag_size + 1 dp = [[0 for _ in range(cols)] for _ in range(rows)] # 初始化dp陣列. for i in range(rows): dp[i][0] = 0 first_item_weight, first_item_value = weight[0], value[0] for j in range(1, cols): if first_item_weight <= j: dp[0][j] = first_item_value for i in range(1, len(weight)): # 物品個數 cur_weight, cur_val = weight[i], value[i] for j in range(1, cols): # 揹包重量 if cur_weight > j: # 說明揹包裝不下當前物品. dp[i][j] = dp[i - 1][j] # 所以不裝當前物品. else: # 定義dp陣列: dp[i][j] 前i個物品裡,放進容量為j的揹包,價值總和最大是多少。 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_weight]+ cur_val) return dp[m][n]
分組揹包
下面的文字是完全參考了這篇題解, 自己寫篇部落格來做個複習鞏固,題解裡有完整程式碼。
上面的購物單,多了個附件,附件依附於主件存在。所以可以把附件的每種情況都當成一個物品,考慮每個物品時要考慮每種可能出現的情況,1、主件,2、主件+附件1,3、主件+附件2,4、主件+附件1+附件2,不一定每種情況都出現,只有當存在附件時才會出現對應的情況。
w[i][k]表示第i個物品的第k種情況,k的取值範圍0~3,分別對應以上4種情況,v[i][k]表示第i個物品對應第k種情況的價值,現在就把購物車問題轉化為了0-1揹包問題。只不過這裡物品的重量(這裡是價格)和價值(這裡是價格和優先度的乘積)變成了二維陣列。
狀態轉移方程可以定義為
dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i][k]]+v[i][k])
dp[i-1][j]表示當前物品不放入揹包,w[i][k]表示第i個主件對應第k種情況,即當前第i個物品的4中情況中價值最大的要麼放入揹包,要麼不放入揹包。
關鍵程式碼:
'''
w,v分別為考慮了附件幾種情況的重量(這裡是價格)和價值(這裡是價格和重要度的乘積)陣列
如對於下面的5個物品,物品2和3從屬於物品1:
'800 2 0',
'400 5 1',
'300 5 1',
'400 3 0',
'500 2 0'
主件1的情況有4種:1、主件,2、主件+附件1,3、主件+附件2,4、主件+附件1+附件2,
對應的w= [[], [800, 1200, 1100, 1500], [400], [500]],v = [[], [1600, 3600, 3100, 5100], [1200], [1000]]。
w[1]即為主件1的價格,len(w[1]) == 4, 分別對應主件1的4種情況。
'''
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
max_i = dp[i-1][j]
for k in range(len(w[i])):
if j-w[i][k]>=0:
max_i = max(max_i, dp[i-1][j-w[i][k]]+v[i][k])
dp[i][j] = max_i
print(dp[m][n])
分組揹包和01揹包的差異
所以分組揹包跟01揹包相比就是:
- 要多處理一下分組的問題
- 遞推那裡是二維陣列了,要考慮每種物品的附件情況
處理分組,把價格和重要度*價格變成二維陣列
- 定義一個主件字典,一個附件字典。主件字典的鍵為物品編號,值為價格和重要度。附件字典鍵為主件編號,值為所有附件的價格和重要度,所以長度>=1。
- 對於每個輸入的‘價格 重要度 從屬’,如果從屬不為0, 則加入附件字典;如果為0,加入主件字典
primary, annex = {}, {} for i in range(1,m+1): x, y, z = map(int, input().split()) if z==0:#主件 primary[i] = [x, y] else:#附件 if z in annex:#第二個附件 annex[z].append([x, y]) else:#第一個附件 annex[z] = [[x,y]]
- 定義價格和重要度*價格的二維陣列w, v= [[]], [[]]
- 定義臨時變數w_tmp,v_tmp=[],[],對於主件裡的每個元素:
- 把每個元素的value加入w_tmp, v_tmp
w_temp.append(primary[key][0])#1、主件 v_temp.append(primary[key][0]*primary[key][1])
- 對於主件裡的每個元素,看鍵是否存在於附件字典,如果有,則分別把附件1,附件2,附件1+附件2計算後的w和v加入w_tmp, v_tmp
if key in annex:# 存在主件 w_temp.append(w_temp[0]+annex[key][0][0])#2、主件+附件1 v_temp.append(v_temp[0]+annex[key][0][0]*annex[key][0][1]) if len(annex[key])>1:#存在兩主件 w_temp.append(w_temp[0]+annex[key][1][0])#3、主件+附件2 v_temp.append(v_temp[0]+annex[key][1][0]*annex[key][1][1]) w_temp.append(w_temp[0]+annex[key][0][0]+annex[key][1][0])#3、主件+附件1+附件2 v_temp.append(v_temp[0]+annex[key][0][0]*annex[key][0][1]+annex[key][1][0]*annex[key][1][1])
- 最後把w_tmp,v_tmp新增到w,v。每個主件都操作一次,所以最後w,v的長度即為主件的長度。
- 把每個元素的value加入w_tmp, v_tmp
遞推dp的時候,對於有附件主件的額外處理
for i in range(1,m+1):
for j in range(10,n+1,10):#物品的價格是10的整數倍
max_i = dp[i-1][j]
for k in range(len(w[i])):
if j-w[i][k]>=0:
max_i = max(max_i, dp[i-1][j-w[i][k]]+v[i][k])
dp[i][j] = max_i