动态规划:01背包问题(二)

上篇博客动态规划: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

思路:

动规五部曲分析如下:

  1. 确定dp数组的定义

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

  1. 一维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的维度去掉了。

  1. 一维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就可以了。

  1. 一维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])

这样大家就会明显看出来遍历的差异了。

  1. 举例推导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())  # 输出最大价值