算法笔记之分组背包(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解法的一般步骤如下:
- 定义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