上篇博客动态规划:01背包问题(一)将的是用二维数组来解决,而本篇博客就是把二维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](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
题目:
46. 携带研究材料
时间限制:5.000S 空间限制:128MB
题目描述:
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
输入描述:
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出描述:
输出一个整数,代表小明能够携带的研究材料的最大价值。
输入示例:
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例:
5
提示信息:
小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。
数据范围:
1 <= N <= 1000
1 <= M <= 1000
研究材料占用空间和价值都小于等于 1000
思路:
动规五部曲分析如下:
- 确定dp数组的定义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
- 一维dp数组的递推公式
dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,
所以递归公式为:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。
- 一维dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
一维dp数组的初始化还是比二维dp数组初始化简单很多,只需全部初始化为0即可。
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
- 一维dp数组遍历顺序
代码如下:
# 动态规划求解最大价值 for i in range(bag_nums): # 遍历每个物品 for j in range(bag_weight, weight[i] - 1, -1): # 从后往前遍历背包重量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) # 更新当前背包重量对应的最大价值
这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
为什么呢?
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
(如何这里读不懂,大家就要动手试一试了,空想还是不靠谱的,实践出真知!)
再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?
不可以!
因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
(这里如果读不懂,就再回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!)
所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!,这一点大家一定要注意。
这里还有一个比较难理解的地方,就是为什么背包的倒序遍历是从 bag_weight 遍历到 weight[i] - 1而不是遍历到 0?
其实遍历到weight[i]的过程就是把 j < weight[i] 的结果排除在外了,每趟循环只需覆盖需要变化的dp数组的值即可,这么说还是比较抽象,大家看下面的表格和图来理解
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
以上述表格数据为例每次循环时变量的取值:
i | weight[i] | j |
---|---|---|
0 | 1 | 4,3,2,1 |
1 | 3 | 4,3 |
2 | 4 | 4 |
每一趟循环dp数组的变化:
因为只有3个物品,所以有三次大循环(最外层for循环),第一次大循环改变dp数组的后四个值,第二次改变后两个,第三次改变最后一个(因为还是35最大,所以值没有变)
相信根据上述表格和图片大家能进一步理解遍历的过程了
上述中的代码其实还可以写成这样:
for i in range(bag_nums): for j in range(bag_weight, 0, -1): if j >= weight[i]: dp[j] = max(dp[j], dp[j-weight[i]] + vals[i])
这样大家就会明显看出来遍历的差异了。
- 举例推导dp数组
一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
代码及详注释:
def result(): # 读取输入的数据 N = [int(x) for x in input().split()] # 输入背包数量和背包总重量 weight = [int(x) for x in input().split()] # 输入每个物品的重量 value = [int(x) for x in input().split()] # 输入每个物品的价值 bag_nums = N[0] # 背包数量 bag_weight = N[1] # 背包总重量 dp = [0] * (bag_weight + 1) # 创建一个数组用于记录每个背包重量对应的最大价值 # 动态规划求解最大价值 for i in range(bag_nums): # 遍历每个物品 for j in range(bag_weight, weight[i] - 1, -1): # 从后往前遍历背包重量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) # 更新当前背包重量对应的最大价值 return dp[bag_weight] # 返回背包总重量对应的最大价值 if __name__ == '__main__': print(result()) # 输出最大价值