献芹奏曝-Python面试题-算法-背包问题
上一篇:献芹奏曝-Python面试题
开篇的话:本文目的是收集和归纳力扣上的算法题,希望用python语言,竭我所能做到思路最清奇、代码最简洁、方法最广泛、性能最高效,了解常见题目,找到最利于记忆的答案,更加从容的应对面试。希望广思集益,共同进步。
一、0 1 背包
一言蔽之:每类物品数量只有一个,选还是不选,这是一个问题。To be, or not to be: that is the question。
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
1:背包分析
1.1 问题描述
现在有4个物品,小偷背包最大容量为8,怎么可以偷得价值最多的物品?
物品编号 | 重量 | 价值 |
---|---|---|
1 | 2 | 3 |
2 | 3 | 4 |
3 | 4 | 5 |
4 | 5 | 8 |
1.2 二维dp数组解决背包问题
1.2.1.确定dp数组以及下标含义
通过使用二维数组dp[i][j]:表示从下标为[0-i]的物品里任意取,放入容量为j的背包,价值总和最大是多少。时刻要牢记这个dp数组的含义,
背包重量 0 | 背包重量 1 | 背包重量 2 | 背包重量 3 | 背包重量 4 | 背包重量 5 | 背包重量 6 | 背包重量 7 | 背包重量 8 | |
---|---|---|---|---|---|---|---|---|---|
物品 1 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
物品 2 | 0 | ||||||||
物品 3 | 0 | ||||||||
物品 4 | 0 |
1.2.2.确定递推公式
有两个方向可以推出来递推公式
- 不放物品i:由dp[i-1][j]推出,即背包容量为j,里面不放物品i的最大价值就是 dp[i-1][j]。这个其实很好理解,为什么不放物品i呢?放不下呗,背包容量剩余2当前物品i重量是3。那此时最大价值依然和前面相同。
- 放物品i:由dp[i-1][j]推出,即背包容量为j,里面不放物品i的最大价值就是 dp[i-1][j-weight[i]]+value[i]。这个其实也很好理解,放了物品i之后,物品i的重量是weight[i],价值是value[i]。那么背包的剩余重量就是j-weight[i]。那么一下子就回到了原来上一步的问题,求dp[i-1][j-weight[i]]的最大价值。
所以,递推公式是求放物品和不放物品时的最大值,即:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
1.2.3.dp数组初始化
上面的图表中已经初始化了背包重量为0和只放物品1时的数据。ps:初始化一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
从递归公式:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) 可以看出来dp[i][j]是由左上方的数值推导出来的,空格中的数据其实是什么已不再重要,反正都会覆盖。
1.2.4.确定遍历顺序
问题来了,先遍历物品还是先遍历背包重量呢?其实都可以,最好是先遍历物品。
为什么都可以,其实很好理解。根据递推公式:取得是左上方数据。行列转换之后对其值无影响。
先遍历物品,其实就是按行慢慢填充;先遍历背包,其实就是按列慢慢填充。
1.2.5.代码示例
def bag_2_wei(bag_size, wp_info): dp = [[0] * (bag_size + 1) for j in range(len(wp_info))] # 01 初始化dp数组 for i in range(len(dp)): # 初始化列:背包重量是0的价值 dp[i][0] = 0 first_item = wp_info.get(1) first_item_weight, first_item_value = first_item.get("weight"), first_item.get("value") for j in range(len(dp[0])): # 初始化行,只放第一个物品的价值 if first_item_weight <= j: dp[0][j] = first_item_value # 02 更新数组:先遍历物品,再遍历背包 for i in range(1, len(dp)): for j in range(1, len(dp[0])): # 物品的id是从1 开始,所以等于当前j+1 cur_item = wp_info.get(i + 1) if cur_item.get("weight") > j: # 当前物品数量 比 背包数量大,说明背包装不下 dp[i][j] = dp[i - 1][j] else: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_item.get("weight")] + cur_item.get("value")) print(dp) wp_info = { 1: {"weight": 2, "value": 3}, 2: {"weight": 3, "value": 4}, 3: {"weight": 4, "value": 5}, 4: {"weight": 5, "value": 8}, } bag_size = 8 bag_2_wei(bag_size, wp_info) """ [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 0, 0, 0, 0, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 12]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 12]] """
""" 01背包 """ def bag_2_wei_wp_first(bag_size, wp_info): dp = [[0] * (bag_size + 1) for j in range(len(wp_info))] # 01 初始化dp数组 for i in range(len(dp)): # 初始化列:背包重量是0的价值 dp[i][0] = 0 first_item = wp_info.get(1) first_item_weight, first_item_value = first_item.get("weight"), first_item.get("value") for j in range(len(dp[0])): # 初始化行,只放第一个物品的价值 if first_item_weight <= j: dp[0][j] = first_item_value # 02 更新数组:先遍历物品,再遍历背包 for i in range(1, len(dp)): for j in range(1, len(dp[0])): # 物品的id是从1 开始,所以等于当前j+1 cur_item = wp_info.get(i + 1) if cur_item.get("weight") > j: # 当前物品数量 比 背包数量大,说明背包装不下 dp[i][j] = dp[i - 1][j] else: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_item.get("weight")] + cur_item.get("value")) print(dp) def bag_2_wei_bag_first(bag_size, wp_info): dp = [[0] * (bag_size + 1) for j in range(len(wp_info))] # 01 初始化dp数组 for i in range(len(dp)): # 初始化列:背包重量是0的价值 dp[i][0] = 0 first_item = wp_info.get(1) first_item_weight, first_item_value = first_item.get("weight"), first_item.get("value") for j in range(len(dp[0])): # 初始化行,只放第一个物品的价值 if first_item_weight <= j: dp[0][j] = first_item_value # 02 更新数组:先遍历物品,再遍历背包 for j in range(1, len(dp[0])): for i in range(1, len(dp)): # 物品的id是从1 开始,所以等于当前j+1 cur_item = wp_info.get(i + 1) if cur_item.get("weight") > j: # 当前物品数量 比 背包数量大,说明背包装不下 dp[i][j] = dp[i - 1][j] else: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_item.get("weight")] + cur_item.get("value")) print(dp) wp_info = { 1: {"weight": 2, "value": 3}, 2: {"weight": 3, "value": 4}, 3: {"weight": 4, "value": 5}, 4: {"weight": 5, "value": 8}, } bag_size = 8 bag_2_wei_bag_first(bag_size, wp_info) """ [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 0, 0, 0, 0, 0, 0], [0, 0, 3, 0, 0, 0, 0, 0, 0], [0, 0, 3, 0, 0, 0, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 0, 0, 0, 0, 0], [0, 0, 3, 4, 0, 0, 0, 0, 0], [0, 0, 3, 4, 0, 0, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 0, 0, 0, 0], [0, 0, 3, 4, 5, 0, 0, 0, 0], [0, 0, 3, 4, 5, 0, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 0, 0, 0], [0, 0, 3, 4, 5, 7, 0, 0, 0], [0, 0, 3, 4, 5, 8, 0, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 0, 0], [0, 0, 3, 4, 5, 7, 8, 0, 0], [0, 0, 3, 4, 5, 8, 8, 0, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 0], [0, 0, 3, 4, 5, 7, 8, 9, 0], [0, 0, 3, 4, 5, 8, 8, 11, 0]] [[0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 12]] """
1.3 一维dp数组解决背包问题
通过滚动数组,把二维dp降为一维dp。
二维数组递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
如果把dp[i-1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i ][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i-1]这一层拷贝到把dp[i],不如只用一个一维数组,只用dp[j](一维数组,可以理解成一个滚动数组即:重复利用上一层)。
1.3.1.确定dp数组以及下标含义
通过使用一维数组dp[j]:表示容量为j的背包,价值总和最大是多少。
1.3.2.确定递推公式
dp[j]可以通过dp[j-weight[i]] 推导出来。
有两个方向可以推出来递推公式
- 不放物品i:dp[j-weight[i]] 表示容量为 j-weight[i] 的背包所背的最大价值。
- 放物品i:dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。
所以,递推公式是求放物品和不放物品时的最大值,即:dp[[j] = max(dp[j],dp[j-weight[i]]+value[i])
1.3.3.dp数组初始化
初始化一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0] 就应该是0,
从递归公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) 可以看出来都初始为0即可。
1.3.4.确定遍历顺序
这里的遍历顺序及其讲究,先公布正确答案:1:先遍历物品,再遍历背包。2:背包倒序遍历。
def bag_1_wei_wp_first(bag_size, wp_info): dp = [0] * (bag_size + 1) # 01 初始化dp数组,都为0 # 02 更新数组:先遍历物品,再遍历背包 for i in range(len(wp_info)): cur_item_weight = wp_info.get(i + 1).get("weight") # 物品的id是从1 开始,所以等于当前i+1 for j in range(len(dp) - 1, cur_item_weight - 1, -1): # 遍历背包容量,要倒序 dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value")) print(dp)
问题1:为什么是倒序遍历背包?
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
倒序 | 背包重量 | ||||||||
初始化 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
物品1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 |
物品1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 3 |
物品1 | 0 | 0 | 0 | 0 | 0 | 0 | 3 | 3 | 3 |
物品1 | 0 | 0 | 0 | 0 | 0 | 3 | 3 | 3 | 3 |
物品1 | 0 | 0 | 0 | 0 | 3 | 3 | 3 | 3 | 3 |
物品1 | 0 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 |
物品1 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
正序 | 背包重量 | ||||||||
初始化 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
物品1 | 0 | 0 | 3 | 0 | 0 | 0 | 0 | 0 | 0 |
物品1 | 0 | 0 | 3 | 3 | 0 | 0 | 0 | 0 | 0 |
物品1 | 0 | 0 | 3 | 3 | 6 | 0 | 0 | 0 | 0 |
物品1 | 0 | 0 | 3 | 3 | 6 | 6 | 0 | 0 | 0 |
物品1 | 0 | 0 | 3 | 3 | 6 | 6 | 9 | 0 | 0 |
物品1 | 0 | 0 | 3 | 3 | 6 | 6 | 9 | 9 | 0 |
物品1 | 0 | 0 | 3 | 3 | 6 | 6 | 9 | 9 | 12 |
可以看到正序中首次出现问题的d[4]
从递归公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) 即:max(d[4],d[4-2]+3)。即 max(0,6)。物品1被重复放了两次,后面就开始重复放3次(9)、4次(12)。
问题2:为什么先遍历物品,能不能先遍历背包?
背包0 [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] 背包1 [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] 背包2 [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 0, 0, 0, 0, 0, 0] 背包3 [0, 0, 3, 3, 0, 0, 0, 0, 0] [0, 0, 3, 4, 0, 0, 0, 0, 0] [0, 0, 3, 4, 0, 0, 0, 0, 0] [0, 0, 3, 4, 0, 0, 0, 0, 0] 背包4 [0, 0, 3, 4, 6, 0, 0, 0, 0] [0, 0, 3, 4, 6, 0, 0, 0, 0] [0, 0, 3, 4, 6, 0, 0, 0, 0] [0, 0, 3, 4, 6, 0, 0, 0, 0] 背包5 [0, 0, 3, 4, 6, 7, 0, 0, 0] [0, 0, 3, 4, 6, 7, 0, 0, 0] [0, 0, 3, 4, 6, 7, 0, 0, 0] [0, 0, 3, 4, 6, 8, 0, 0, 0] 背包6 [0, 0, 3, 4, 6, 8, 9, 0, 0] [0, 0, 3, 4, 6, 8, 9, 0, 0] [0, 0, 3, 4, 6, 8, 9, 0, 0] [0, 0, 3, 4, 6, 8, 9, 0, 0] 背包7 [0, 0, 3, 4, 6, 8, 9, 11, 0] [0, 0, 3, 4, 6, 8, 9, 11, 0] [0, 0, 3, 4, 6, 8, 9, 11, 0] [0, 0, 3, 4, 6, 8, 9, 11, 0] 背包8 [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12]
背包8 [0, 0, 0, 0, 0, 0, 0, 0, 3] [0, 0, 0, 0, 0, 0, 0, 0, 4] [0, 0, 0, 0, 0, 0, 0, 0, 5] [0, 0, 0, 0, 0, 0, 0, 0, 8] 背包7 [0, 0, 0, 0, 0, 0, 0, 3, 8] [0, 0, 0, 0, 0, 0, 0, 4, 8] [0, 0, 0, 0, 0, 0, 0, 5, 8] [0, 0, 0, 0, 0, 0, 0, 8, 8] 背包6 [0, 0, 0, 0, 0, 0, 3, 8, 8] [0, 0, 0, 0, 0, 0, 4, 8, 8] [0, 0, 0, 0, 0, 0, 5, 8, 8] [0, 0, 0, 0, 0, 0, 8, 8, 8] 背包5 [0, 0, 0, 0, 0, 3, 8, 8, 8] [0, 0, 0, 0, 0, 4, 8, 8, 8] [0, 0, 0, 0, 0, 5, 8, 8, 8] [0, 0, 0, 0, 0, 8, 8, 8, 8] 背包4 [0, 0, 0, 0, 3, 8, 8, 8, 8] [0, 0, 0, 0, 4, 8, 8, 8, 8] [0, 0, 0, 0, 5, 8, 8, 8, 8] [0, 0, 0, 0, 5, 8, 8, 8, 8] 背包3 [0, 0, 0, 3, 5, 8, 8, 8, 8] [0, 0, 0, 4, 5, 8, 8, 8, 8] [0, 0, 0, 4, 5, 8, 8, 8, 8] [0, 0, 0, 4, 5, 8, 8, 8, 8] 背包2 [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] 背包1 [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8]
1.3.5.代码示例
def bag_1_wei_wp_first(bag_size, wp_info): dp = [0] * (bag_size + 1) # 01 初始化dp数组,都为0 # 02 更新数组:先遍历物品,再遍历背包 for i in range(len(wp_info)): print("物品" + str(i + 1)) cur_item_weight = wp_info.get(i + 1).get("weight") # 物品的id是从1 开始,所以等于当前i+1 for j in range(len(dp) - 1, cur_item_weight - 1, -1): # 遍历背包容量,要倒序 dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value")) print(dp) wp_info = { 1: {"weight": 2, "value": 3}, 2: {"weight": 3, "value": 4}, 3: {"weight": 4, "value": 5}, 4: {"weight": 5, "value": 8}, } bag_size = 8 bag_1_wei_wp_first(bag_size, wp_info) """ 物品1 [0, 0, 0, 0, 0, 0, 0, 0, 3] [0, 0, 0, 0, 0, 0, 0, 3, 3] [0, 0, 0, 0, 0, 0, 3, 3, 3] [0, 0, 0, 0, 0, 3, 3, 3, 3] [0, 0, 0, 0, 3, 3, 3, 3, 3] [0, 0, 0, 3, 3, 3, 3, 3, 3] [0, 0, 3, 3, 3, 3, 3, 3, 3] 物品2 [0, 0, 3, 3, 3, 3, 3, 3, 7] [0, 0, 3, 3, 3, 3, 3, 7, 7] [0, 0, 3, 3, 3, 3, 7, 7, 7] [0, 0, 3, 3, 3, 7, 7, 7, 7] [0, 0, 3, 3, 4, 7, 7, 7, 7] [0, 0, 3, 4, 4, 7, 7, 7, 7] 物品3 [0, 0, 3, 4, 4, 7, 7, 7, 9] [0, 0, 3, 4, 4, 7, 7, 9, 9] [0, 0, 3, 4, 4, 7, 8, 9, 9] [0, 0, 3, 4, 4, 7, 8, 9, 9] [0, 0, 3, 4, 5, 7, 8, 9, 9] 物品4 [0, 0, 3, 4, 5, 7, 8, 9, 12] [0, 0, 3, 4, 5, 7, 8, 11, 12] [0, 0, 3, 4, 5, 7, 8, 11, 12] [0, 0, 3, 4, 5, 8, 8, 11, 12] """
def bag_1_wei_wp_first_error(bag_size, wp_info): dp = [0] * (bag_size + 1) # 01 初始化dp数组,都为0 # 02 更新数组:先遍历物品,再遍历背包 for i in range(len(wp_info)): print("物品" + str(i + 1)) cur_item_weight = wp_info.get(i + 1).get("weight") # 物品的id是从1 开始,所以等于当前i+1 for j in range(cur_item_weight, len(dp)): # 遍历背包容量,正序遍历 dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value")) print(dp) wp_info = { 1: {"weight": 2, "value": 3}, 2: {"weight": 3, "value": 4}, 3: {"weight": 4, "value": 5}, 4: {"weight": 5, "value": 8}, } bag_size = 8 bag_1_wei_wp_first_error(bag_size, wp_info) """ 物品1 [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 3, 0, 0, 0, 0, 0] [0, 0, 3, 3, 6, 0, 0, 0, 0] [0, 0, 3, 3, 6, 6, 0, 0, 0] [0, 0, 3, 3, 6, 6, 9, 0, 0] [0, 0, 3, 3, 6, 6, 9, 9, 0] [0, 0, 3, 3, 6, 6, 9, 9, 12] 物品2 [0, 0, 3, 4, 6, 6, 9, 9, 12] [0, 0, 3, 4, 6, 6, 9, 9, 12] [0, 0, 3, 4, 6, 7, 9, 9, 12] [0, 0, 3, 4, 6, 7, 9, 9, 12] [0, 0, 3, 4, 6, 7, 9, 10, 12] [0, 0, 3, 4, 6, 7, 9, 10, 12] 物品3 [0, 0, 3, 4, 6, 7, 9, 10, 12] [0, 0, 3, 4, 6, 7, 9, 10, 12] [0, 0, 3, 4, 6, 7, 9, 10, 12] [0, 0, 3, 4, 6, 7, 9, 10, 12] [0, 0, 3, 4, 6, 7, 9, 10, 12] 物品4 [0, 0, 3, 4, 6, 8, 9, 10, 12] [0, 0, 3, 4, 6, 8, 9, 10, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] """
def bag_1_wei_bag_first_asc(bag_size, wp_info): dp = [0] * (bag_size + 1) # 01 初始化dp数组,都为0 # 02 更新数组:先遍历背包,再遍历物品 for j in range(len(dp)): # 背包正序 print("背包" + str(j)) for i in range(len(wp_info)): cur_item_weight = wp_info.get(i + 1).get("weight") # 物品的id是从1 开始,所以等于当前i+1 if cur_item_weight > j: dp[j] = max(dp[j], dp[j - 1]) else: # 遍历背包容量, dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value")) print(dp) wp_info = { 1: {"weight": 2, "value": 3}, 2: {"weight": 3, "value": 4}, 3: {"weight": 4, "value": 5}, 4: {"weight": 5, "value": 8}, } bag_size = 8 bag_1_wei_bag_first_asc(bag_size, wp_info) """ 背包0 [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] 背包1 [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0] 背包2 [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 0, 0, 0, 0, 0, 0] [0, 0, 3, 0, 0, 0, 0, 0, 0] 背包3 [0, 0, 3, 3, 0, 0, 0, 0, 0] [0, 0, 3, 4, 0, 0, 0, 0, 0] [0, 0, 3, 4, 0, 0, 0, 0, 0] [0, 0, 3, 4, 0, 0, 0, 0, 0] 背包4 [0, 0, 3, 4, 6, 0, 0, 0, 0] [0, 0, 3, 4, 6, 0, 0, 0, 0] [0, 0, 3, 4, 6, 0, 0, 0, 0] [0, 0, 3, 4, 6, 0, 0, 0, 0] 背包5 [0, 0, 3, 4, 6, 7, 0, 0, 0] [0, 0, 3, 4, 6, 7, 0, 0, 0] [0, 0, 3, 4, 6, 7, 0, 0, 0] [0, 0, 3, 4, 6, 8, 0, 0, 0] 背包6 [0, 0, 3, 4, 6, 8, 9, 0, 0] [0, 0, 3, 4, 6, 8, 9, 0, 0] [0, 0, 3, 4, 6, 8, 9, 0, 0] [0, 0, 3, 4, 6, 8, 9, 0, 0] 背包7 [0, 0, 3, 4, 6, 8, 9, 11, 0] [0, 0, 3, 4, 6, 8, 9, 11, 0] [0, 0, 3, 4, 6, 8, 9, 11, 0] [0, 0, 3, 4, 6, 8, 9, 11, 0] 背包8 [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] [0, 0, 3, 4, 6, 8, 9, 11, 12] """
def bag_1_wei_bag_first_desc(bag_size, wp_info): dp = [0] * (bag_size + 1) # 01 初始化dp数组,都为0 # 02 更新数组:先遍历背包,再遍历物品 for j in range(len(dp) - 1, 0, -1): print("背包" + str(j)) # 背包倒序 for i in range(len(wp_info)): cur_item_weight = wp_info.get(i + 1).get("weight") # 物品的id是从1 开始,所以等于当前i+1 if cur_item_weight > j: dp[j] = max(dp[j], dp[j - 1]) else: # 遍历背包容量, dp[j] = max(dp[j], dp[j - cur_item_weight] + wp_info.get(i + 1).get("value")) print(dp) wp_info = { 1: {"weight": 2, "value": 3}, 2: {"weight": 3, "value": 4}, 3: {"weight": 4, "value": 5}, 4: {"weight": 5, "value": 8}, } bag_size = 8 bag_1_wei_bag_first_desc(bag_size, wp_info) """ 背包8 [0, 0, 0, 0, 0, 0, 0, 0, 3] [0, 0, 0, 0, 0, 0, 0, 0, 4] [0, 0, 0, 0, 0, 0, 0, 0, 5] [0, 0, 0, 0, 0, 0, 0, 0, 8] 背包7 [0, 0, 0, 0, 0, 0, 0, 3, 8] [0, 0, 0, 0, 0, 0, 0, 4, 8] [0, 0, 0, 0, 0, 0, 0, 5, 8] [0, 0, 0, 0, 0, 0, 0, 8, 8] 背包6 [0, 0, 0, 0, 0, 0, 3, 8, 8] [0, 0, 0, 0, 0, 0, 4, 8, 8] [0, 0, 0, 0, 0, 0, 5, 8, 8] [0, 0, 0, 0, 0, 0, 8, 8, 8] 背包5 [0, 0, 0, 0, 0, 3, 8, 8, 8] [0, 0, 0, 0, 0, 4, 8, 8, 8] [0, 0, 0, 0, 0, 5, 8, 8, 8] [0, 0, 0, 0, 0, 8, 8, 8, 8] 背包4 [0, 0, 0, 0, 3, 8, 8, 8, 8] [0, 0, 0, 0, 4, 8, 8, 8, 8] [0, 0, 0, 0, 5, 8, 8, 8, 8] [0, 0, 0, 0, 5, 8, 8, 8, 8] 背包3 [0, 0, 0, 3, 5, 8, 8, 8, 8] [0, 0, 0, 4, 5, 8, 8, 8, 8] [0, 0, 0, 4, 5, 8, 8, 8, 8] [0, 0, 0, 4, 5, 8, 8, 8, 8] 背包2 [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] 背包1 [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] [0, 0, 3, 4, 5, 8, 8, 8, 8] """
二、0 1 背包 之 二维费用背包问题
1.1 问题描述
现在有4个物品,小偷背包支持最大重量为8,体积是20,怎么可以偷得价值最多的物品?(和一中数据一样,只是多了体积限制)
物品编号 | 重量 | 体积 | 价值 |
---|---|---|---|
1 | 2 | 4 | 3 |
2 | 3 | 6 | 4 |
3 | 4 | 7 | 5 |
4 | 5 | 10 | 8 |
1.2 二维dp数组解决背包问题
1.2.1.确定dp数组以及下标含义
通过使用二维数组dp[v][w]:表示从下标为[0-w]的重量下,放入容量为v的背包,价值总和最大是多少。时刻要牢记这个dp数组的含义,
背包重量 0 | 背包重量 1 | 背包重量 2 | 背包重量 3 | 背包重量 4 | 背包重量 5 | 背包重量 6 | 背包重量 7 | 背包重量 8 | |
---|---|---|---|---|---|---|---|---|---|
背包体积 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
背包体积 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
背包体积 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
背包体积 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
背包体积 4 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
背包体积 5 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
背包体积 6 | 0 | 0 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
1.2.2.确定递推公式
有两个方向可以推出来递推公式
- 物品不变:由dp[v][w]推出,即背包体积为v,里面最大价值就是 dp[v][w]。
- 物品变化:由dp[v][w]推出,即背包体积为v,里面放入物品i,最大价值就是 dp[v-volum[i]][k-weight[i]]+value[i]。很好理解:把i占据的空间、重量去掉,把i的价值加上
所以,递推公式是求放物品和不放物品时的最大值,即:dp[v][w] = max(dp[v][w], dp[v-volum[i]][k-weight[i]]+value[i])
1.2.3.dp数组初始化
上面的图表中已经初始化了背包重量为0和体积为0时的数据。ps:初始化一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
从递归公式:dp[v][w] = max(dp[v][w], dp[v-volum[i]][k-weight[i]]+value[i]) 可以看出来dp[v][w]是由左上方的数值推导出来的,空格中的数据其实是什么已不再重要,反正都会覆盖。
1.2.4.确定遍历顺序
问题来了,先遍历体积还是先遍历背包重量呢?其实都可以,最好是先遍历数据范围比较小的。
为什么都可以,其实很好理解。根据递推公式:取得是左上方数据。行列转换之后对其值无影响。
先遍历体积,其实就是按行慢慢填充;先遍历重量,其实就是按列慢慢填充。
1.2.5.代码示例
""" 01背包 - 二维费用 """ def bag_2_wei_cost_first(bag_max_weight, bag_max_volume, wp_info): # 初始化数据 dp = [[0] * (bag_max_weight + 1) for k in range(bag_max_volume + 1)] # 循环物品 for item in wp_info: # 02 更新数组:先遍历重量,再遍历体积 for w in range(bag_max_weight, 0, -1): if w < item.get("weight"): # 如果背包的重量 小于 物品的重量,那就不能好好玩耍了,由于是倒序排列也没必要continue,直接break break for v in range(bag_max_volume, 0, -1): if v < item.get("volume"): # 如果背包的体积 小于 物品的体积,那也不能好好玩耍了,由于是倒序排列也没必要continue,直接break break dp[v][w] = max(dp[v][w], dp[v - item.get("volume")][w - item.get("weight")] + item.get("value")) print(dp) wp_info = [ {"weight": 2, "volume": 4, "value": 3}, {"weight": 3, "volume": 6, "value": 4}, {"weight": 4, "volume": 7, "value": 5}, {"weight": 5, "volume": 10, "value": 8}, ] bag_max_weight = 8 bag_max_volume = 20 bag_2_wei_cost_first(bag_max_weight, bag_max_volume, wp_info) """ [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3]] [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7], [0, 0, 3, 4, 4, 7, 7, 7, 7]] [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 7, 7, 7, 7], [0, 0, 3, 4, 5, 7, 8, 8, 8], [0, 0, 3, 4, 5, 7, 8, 8, 8], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9], [0, 0, 3, 4, 5, 7, 8, 9, 9]] [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 3, 3, 3, 3, 3, 3], [0, 0, 3, 4, 4, 4, 4, 4, 4], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 5, 5, 5, 5], [0, 0, 3, 4, 5, 8, 8, 8, 8], [0, 0, 3, 4, 5, 8, 8, 8, 8], [0, 0, 3, 4, 5, 8, 8, 8, 8], [0, 0, 3, 4, 5, 8, 8, 9, 9], [0, 0, 3, 4, 5, 8, 8, 11, 11], [0, 0, 3, 4, 5, 8, 8, 11, 11], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12], [0, 0, 3, 4, 5, 8, 8, 11, 12]] """
三、0 1 背包 之 分组背包
3:背包分析
3.1 问题描述
现在有4个物品,小偷背包最大容量为8,每类产品只能偷一种,怎么可以偷得价值最多的物品?
物品编号 | 重量 | 价值 | 种类 |
---|---|---|---|
1 | 2 | 3 | 电子产品 |
2 | 3 | 4 | 电子产品 |
3 | 4 | 5 | 食品 |
4 | 5 | 8 | 食品 |
3.2 dp数组解决分组背包问题
3.2.1.确定二维dp数组以及下标含义
通过使用二维数组dp[i][j]:表示从下标为[0-i]的物品组中选择其中一件物品,放入重量为j的背包,价值总和最大是多少。时刻要牢记这个dp数组的含义,
物品组 | 重量0 | 重量1 | 重量2 | 重量3 | 重量4 | 重量5 | 重量6 | 重量7 | 重量8 |
(物品编号1,物品编号2) | 0 | 0 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
(物品编号3,物品编号4) | 0 | 0 | 3 | 4 | 5 | 8 | 8 | 9 | 12 |
3.2.2.确定一维dp数组以及下标含义
由于我们已经熟练的使用了数据降维为一维数组(滚动数组),我们可以直接推出一维数组的递推公式
有两个方向可以推出来递推公式
- 物品不变:由dp[j]推出,即背包重量为j,里面最大价值就是 dp[j]。
- 放入物品:由dp[j]推出,里面放入物品i,最大价值就是 max(dp[j-物品A重量]+物品A价值,dp[j-物品B重量]+物品B价值,......)
所以,递推公式是求放物品和不放物品时的最大值,即:dp[[j] = max(dp[j], max(dp[j-物品A重量]+物品A价值,dp[j-物品B重量]+物品B价值,......)),简化之后去掉一层max
dp[[j] = max(dp[j], dp[j-weight[i]]+value[i],dp[j-weight[i2]]+value[i2],......)
3.2.3.dp数组初始化
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0] 就应该是0,
从递归公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) 可以看出来都初始为0即可。
3.2.4.确定遍历顺序
这里的遍历顺序及其讲究,1:先遍历物品组,再遍历背包。2:背包倒序遍历。3:再遍历背包组的物品。
3.2.5.代码示例
""" 01背包-分组背包 """ def bag_1_wei_wp_first(bag_size, wp_info): dp = [0] * (bag_size + 1) # 01 初始化dp数组,都为0 # 02 更新数组:先遍历物品组,再(倒序)遍历背包,再遍历物品 for wp_group in wp_info: # 遍历背包容量,要倒序 for j in range(bag_size, 0, -1): # 再遍历背包中的物品, for wp in wp_group: if j > wp.get("weight"): dp[j] = max(dp[j], dp[j - wp.get("weight")] + wp.get("value")) print(dp) wp_info = [ # 电子产品 [{"weight": 2, "value": 3}, {"weight": 3, "value": 4}], # 食品 [{"weight": 4, "value": 5}, {"weight": 8, "value": 8}] ] bag_size = 8 bag_1_wei_wp_first(bag_size, wp_info) """ [0, 0, 0, 0, 0, 0, 0, 0, 4] [0, 0, 0, 0, 0, 0, 0, 4, 4] [0, 0, 0, 0, 0, 0, 4, 4, 4] [0, 0, 0, 0, 0, 4, 4, 4, 4] [0, 0, 0, 0, 4, 4, 4, 4, 4] [0, 0, 0, 3, 4, 4, 4, 4, 4] [0, 0, 0, 3, 4, 4, 4, 4, 4] [0, 0, 0, 3, 4, 4, 4, 4, 4] [0, 0, 0, 3, 4, 4, 4, 4, 9] [0, 0, 0, 3, 4, 4, 4, 8, 9] [0, 0, 0, 3, 4, 4, 5, 8, 9] [0, 0, 0, 3, 4, 5, 5, 8, 9] [0, 0, 0, 3, 4, 5, 5, 8, 9] [0, 0, 0, 3, 4, 5, 5, 8, 9] [0, 0, 0, 3, 4, 5, 5, 8, 9] [0, 0, 0, 3, 4, 5, 5, 8, 9] """
X、经典练习题
import sys import copy """ 分析: 一、大方向 典型的01背包问题,即:每个物品只能出现一次,在满足一定条件下,价值最大。 满足花费、数量两重限制 (01背包之二维费用背包) 二、差异点 1:物品之间有主件和附件的内在联系,可以只买主件,不能只买附件 (打包成分组背包处理) 2:价值计算方式:重要度*价格 方案: 一、确定数据格式 wp_info = { # 背包组1 "1":[ {"price":800,"level":2,"p_id":0,"value":1600}, # 只买主件 {"price":1200=800+400,"level":5,"p_id":1,"value":3600=2000+1600}, # 主件和附件一起吗 ] } 二、使用二维数组 dp = [[],[],[]] 三、遍历方案 先物品、 四、确定递推公式 dp[p][c] = max(dp[p][c],dp[p-price[i]][c-1]+value[i]) """ def init_wp_info(): bag_count = 0 # 定义背包中物品数量 bag_price = 0 # 定义背包价格 group_wp = {} wp_info = {} c_id = 0 step = 100 # 定义价格步长,提升遍历速度 for index, line in enumerate(sys.stdin): c_id = index list_line = line.strip().split(" ") if index == 0: bag_price = int(list_line[0]) bag_count = int(list_line[1]) else: wp_info[c_id] = { "price": int(list_line[0]), "count": int(1), "p_id": int(list_line[2]), "value": int(list_line[0]) * int(list_line[1]), } # 将主件创建背包组 if int(list_line[2]) == 0: group_wp.setdefault(c_id, [wp_info[c_id]]) if int(list_line[0]) % 100: step = 10 # 打包=分组 for k, _item in wp_info.items(): group_id = _item.get("p_id") if group_id > 0: c_id += 1 cur_group_wp = copy.deepcopy(group_wp.get(group_id)) # type:dict for item_y in cur_group_wp: # 把附件进行打包 new_wp_info = { "price": item_y.get("price") + _item.get("price"), "count": item_y.get("count") + 1, "p_id": item_y.get("p_id"), "value": item_y.get("value") + _item.get("value"), } group_wp.get(group_id).append(new_wp_info) return group_wp, bag_count, int(bag_price), step def get_max_value(group_wp, bag_count, bag_price, step): dp = [[0] * (bag_count + 1) for p in range(bag_price + 1)] # ps:边界+1 # 遍历物品组 for k, wp_list in group_wp.items(): for c in range(bag_count, 0, -1): # 倒序遍历背包 之 背包物品容纳数量 min_count = wp_list[0].get("count") if min_count > c: break for p in range(bag_price, 0, -step): # 倒序遍历背包 之 背包物品价格,注意这个遍历的步长 min_price = wp_list[0].get("price") if min_price > p: break for ite in wp_list: if ite.get("price") > p: continue dp[p][c] = max( dp[p][c], dp[p - ite.get("price")][c - ite.get("count")] + ite.get("value"), ) return dp[-1][-1] group_wp, bag_count, bag_price, step = init_wp_info() result = get_max_value(group_wp, bag_count, bag_price, step) print(result)
知识点/技巧:1:01背包、分组背包、二维费用背包