1. 程式人生 > 其它 >演算法筆記之分組揹包(HJ16 購物單)

演算法筆記之分組揹包(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]

w[j_2]+ … +v[j_k]*w[j_k]
請你幫助王強計算可獲得的最大的滿意度。

01揹包基礎

最基礎的揹包問題:有n件物品和一個最多能背重量為w 的揹包。第i件物品的重量是weight[i],得到的價值是value[i] 。每件物品只能用一次,求解將哪些物品裝入揹包裡物品價值總和最大。
dp解法的一般步驟如下:

  1. 定義dp[i][j]的含義:dp[i][j] 表示從下標為[0-i]的物品裡任意取,放進容量為j的揹包,價值總和最大是多少。
  2. 確定遞推公式:
    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]);
  3. 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.
  4. 確定遍歷順序
    二維陣列先遍歷揹包容量還是先遍歷物品不影響。
  5. 舉例推導驗證
    當揹包容量為3,從編號為0和1的兩個物品裡取,最大價值會是多少?明顯的只能取物品1,此時最大價值是20.
  6. 關鍵程式碼
     '''
     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. 要多處理一下分組的問題
  2. 遞推那裡是二維陣列了,要考慮每種物品的附件情況

處理分組,把價格和重要度*價格變成二維陣列

  1. 定義一個主件字典,一個附件字典。主件字典的鍵為物品編號,值為價格和重要度。附件字典鍵為主件編號,值為所有附件的價格和重要度,所以長度>=1。
  2. 對於每個輸入的‘價格 重要度 從屬’,如果從屬不為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]]
    
  3. 定義價格和重要度*價格的二維陣列w, v= [[]], [[]]
  4. 定義臨時變數w_tmp,v_tmp=[],[],對於主件裡的每個元素:
    1. 把每個元素的value加入w_tmp, v_tmp
          w_temp.append(primary[key][0])#1、主件
          v_temp.append(primary[key][0]*primary[key][1])
      
    2. 對於主件裡的每個元素,看鍵是否存在於附件字典,如果有,則分別把附件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])
      
    3. 最後把w_tmp,v_tmp新增到w,v。每個主件都操作一次,所以最後w,v的長度即為主件的長度。

遞推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