算法笔记之分组背包(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
posted @ 2022-05-17 21:08  水天需  阅读(399)  评论(0编辑  收藏  举报