问题描述
假设我有一个背包,希望在装得下的情况下,尽量装进价值更多的物品。那么我该怎么做呢?
问题抽象
假设背包的容量是m,就假设是4吧
# 表示背包容量4KG
m = 4
可选装进背包的物品有n个,物品的价值存储在prices里,重量存储在weight里
# 表示有3个物品可以选,分别价值是1500 3000 2000
prices = [1500, 3000, 2000]
# 物品重量分别是1kg 4kg 3kg
weight = [1, 4, 3]
显然,我们的目的是填满以下价值矩阵,res[i][j]表示在可选物品有i个的情况下,背包容量在j容量的情况下的最优解:
# 我们需要填满这个n 行 m列的矩阵
res = [[0] * m for _ in range(n)]
物品价值和重量 | 物品\背包容量 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
1500元 1kg | 物品1 | ||||
3000元 4kg | 物品1、物品2 | ||||
2000元 3kg | 物品1/2/3 |
求解过程
显然,第一行是最好求解的。当只有1个物品可选时,只要装得下,只能装它。所以第一行全是1500元。
物品价值和重量 | 物品\背包容量 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
1500元 1kg | 物品1 | 1500 | 1500 | 1500 | 1500 |
3000元 4kg | 物品1、物品2 | ||||
2000元 3kg | 物品1/2/3 |
当可选物品变成2个时,显然,由于物品2的重量是4KG,背包容量小于4的情况下,全都放不下,于是只有在背包容量达到4的时候,可以放一个物品2,这个格子的最优解是3000
物品价值和重量 | 物品\背包容量 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
1500元 1kg | 物品1 | 1500 | 1500 | 1500 | 1500 |
3000元 4kg | 物品1、物品2 | 1500 | 1500 | 1500 | 3000 |
2000元 3kg | 物品1/2/3 |
当可选物品变成3个的时候,在背包容量3的时候,可以选择物品3了,所以这一个格子的值是2000
物品价值和重量 | 物品\背包容量 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
1500元 1kg | 物品1 | 1500 | 1500 | 1500 | 1500 |
3000元 4kg | 物品1、物品2 | 1500 | 1500 | 1500 | 3000 |
2000元 3kg | 物品1/2/3 | 1500 | 1500 | 2000 |
最后一个格子比较特殊,我们来分别考虑
-
如果背包容量-当前最新物品的重量>0,说明还能装其他东西,所以这一个格子可能值是:
当前物品价值+ 上一行的 剩余背包容量最优解。再拿这个值,和上一行的该列比较,大的那个就是最优解
# 注意这里j+1,是因为是j从0开始,而背包容量从1开始 prices[i] + res[i-1][j+1-weight[i]] # res[i][j] = max(res[i-1][j], prices[i] + res[i-1][j+1-weight[i]])
-
如果背包容量-当前最新物品的重量=0,那么说明这时候只能装它了。最优解就是新物品重量和上一行比,大的那一个
res[i][j] = max(res[i - 1][j], prices[i])
-
如果背包容量-当前最新物品的重量<0,那么说明新物品对这个格子没有影响,还是取上一行的值
res[i][j] = res[i-1][j]
经过一番比较,我们可以知道这个格子最优是2000加上上一行的容量为1时的值
物品价值和重量 | 物品\背包容量 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
1500元 1kg | 物品1 | 1500 | 1500 | 1500 | 1500 |
3000元 4kg | 物品1、物品2 | 1500 | 1500 | 1500 | 3000 |
2000元 3kg | 物品1/2/3 | 1500 | 1500 | 2000 | 3500 |
最终代码
经过上面的举例,可以看出,大部分格子都遵循这三种情况。除了第一行,因为没法和上一行比较。最终代码如下:
def max_bag(n: int, m: int, weight: list[int], prices: list[int]):
"""
背包问题
:param n: 可选物品总数量,矩阵的行
:param m: 背包总重量,矩阵的列
:param weight: 物品重量列表
:param prices: 物品价格列表
:return: 返回最大价值
"""
res = [[0] * m for _ in range(n)]
# 第一行
for j in range(m):
if j + 1 - weight[0] >= 0:
res[0][j] = prices[0]
else:
res[0][j] = 0
for i in range(1, n):
for j in range(m):
# 大于零说明装了当前商品之后还有空位,那么最大值就是在res[i-1][j]和
# 当前价值prices[i] + 剩余格子价值
# 这两者之间取最大值
if j + 1 - weight[i] > 0:
res[i][j] = max(res[i-1][j], prices[i] + res[i-1][j+1-weight[i]])
# 等于0,说明刚好能装这一个物品,所以最大值是要么上一行的这个格子,要么就是当前商品价值
elif j + 1 - weight[i] == 0:
res[i][j] = max(res[i - 1][j], prices[i])
# 如果小于0,那就还是取上一行的
else:
res[i][j] = res[i-1][j]
print(res)
return res[n-1][m-1]
可以用如下输入来验证结果:
my_n = 3
my_m = 4
my_weights = [1, 4, 3]
my_prices = [1500, 3000, 2000]
print(max_bag(my_n, my_m, my_weights, my_prices))
一些优化
考虑到可以让列的索引,也就是j直接代表背包的容量,我们在创建矩阵时,可以直接创建一个n+1 * m+1的矩阵,也就是第一列和第一行全为0
# 建立一个 n+1 * m+1的矩阵,这样res[i][j]就代表:第i-1个物品在背包容量为j的时候的最优解
res = [[0] * (m+1) for _ in range(n+1)]
这样做,有3个好处:
- 不需要单独处理第一行了,第一行已经有了,并且全为0
- j可以直接代表背包容量,不需要减1
j - weight[i-1] >=0
的情况,可以合并起来写,这是因为prices[i-1] + res[i-1][j-weight[i-1]]
实际上右边会取到第一列的值,res[i-1][j-weight[i-1]]=0
,相当于max(res[i-1][j], prices[i-1])
优化后的代码如下:注意i是从1开始的,所以使用weight[i-1] 和 prices[i-1]
def max_bag(n: int, m: int, weight: list[int], prices: list[int]):
"""
背包问题
:param n: 可选物品总数量,矩阵的行
:param m: 背包总重量,矩阵的列
:param weight: 物品重量列表
:param prices: 物品价格列表
:return: 返回最大价值
"""
# 建立一个 n+1 * m+1的矩阵,这样res[i][j]就代表:第i个物品在背包容量为j的时候的最优解
res = [[0] * (m+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, m+1):
if j - weight[i-1] >=0:
res[i][j] = max(res[i-1][j], prices[i-1] + res[i-1][j-weight[i-1]])
else:
res[i][j] = res[i-1][j]
print(res)
return res[n][m]
本文参考了:https://blog.csdn.net/bohu83/article/details/91453227