入门01背包问题

前言

最近在luogu刷题,深受背包问题的折磨。作为一个刚开始学习动态规划的蒟蒻,我只能解决简单的背包问题。为了加深本蒟蒻对该题型的理解,我决定将自己目前对01背包的知识整理起来。(有关代码将会使用python来展示)

什么是背包问题

请看以下问题:

小明有一个能装500克物品的背包,现在在他的面前有5个物品,它们的重量和价值如下:

物品1:130g 50元

物品2:200g 75元

物品3:400g 200元

物品4:800g 1000元

物品5:50g 50元

请问小明要如何装入面前的物品,使得背包里的东西价值最大呢?

以上就是一个01背包问题,一般来讲,背包问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。当然背包问题的题干里不一定会有“背包”二字,也不一定有所谓“重量”“价格”,实际做题时,要审准题目,找出隐含的条件。

01背包问题

这是一道经典的01背包问题,摘自洛谷P1048 采药:

01背包问题的特点就是,每个物品只有拿或不拿两种情况。以《采药》为例,给出来的每株药,要么被辰辰采了,要么没采,不存在采了两遍或采了三四遍的情况。

解题方法

接下来,我将以一个简单的例子说明01背包的解题方法,然后我们就可以攻克《采药》了:

为了让条件尽量简单,我们的小明拿出了一个仅能装5g物品的迷你背包,现在他要装这些物品:

物品1:2g 3元;物品2:3g 2元;物品3:3g 5元;物品4:4g 6元;物品5:1g 1元

接下来,让我们假设背包的容量是可以变的,但是不能超过5g

现在我们装入物品1,当背包容量大小在0g到5g之间变化时,能够达到的最大价值可以用以下表格来呈现

好了,接下来我们在上表的基础上,装入物品2(3g 2元);

显然,当背包容量为0,1,2时,无法装下物品2,因此这3个格子能达到的最大价值和上一行一样:

于是我们得到了第一条规则:当背包容量不足以装下现在正在考虑的物品时,当前格子的值和上一行相同

那么当背包容量为3,4,5时怎么办呢;此时将要面临一个抉择:究竟要不要放入物品2。一个很自然的想法就是:如果放入物品2比不放入所能达到的价值更大,那就放,否则就不放。以背包容量为4时作例子,如果要放,那么我们首先要把3g的空间省出来,我们来看看省完3g之后背包里还剩多少价值

怎么看?4g-3g=1g,上一行对应1g的格子就是省完3g后剩余的价值(红圈),把这个值加上物品2的价值,即可得到情况A:放入物品2,此种情况达到的价值为2元

 但是,放入物品2不一定就是好的,情况B:不放入物品2能得到更大的价值,那么不放入物品2达到的价值在哪看,显然就是上一行对应的值,可以看到不放入物品2得到的价值为3元

于是我们得到了第二条规则:当背包容量足以放入现在正在考虑的物品时,对比放与不放两种情况,取两者之中较大的一个

利用这两条规则我们可以补充完全上表:

 接下来的物品3、4、5也采取同样的操作,可以得到如下表格(建议初次接触背包问题的同学在纸上模拟一下这个表生成的过程):

 可以看到,右下角的值就是小明所能获得的最大价值,也就是8元。以这个例子为基础,我们就能写出《采药》的题解。

例题《采药》的解法

为了解决《采药》,我们可以像上面的例子那样,画一个表格。但是怎么用python代码来表示这个表格呢?一种简单易行的方案是构造一个2维数组 dp[i][j] 来表示,其中 i 对应物品编号,j 对应背包容量。例如上表就可以写成这样一个二维数组:

dp = [[0,0,3,3,3,3],
[0,0,3,3,3,5],
[0,0,3,5,5,8],
[0,0,3,5,6,8],
[0,1,3,5,6,8]]

让我们回看《采药》的题干:

看完题目,先把数据的输入解决了,因为每株草药都有“花费时间”和“价值”两个属性,我们用列表cost记录花费时间,用列表value记录价值

T, M = map(int,input().split())
cost = []
value = []
for i in range(M):
    a, b = map(int,input().split())
    cost.append(a)
    value.append(b)

分析题目,发现这是一个01背包问题,其中T(总共能用来采药的时间)可以看作背包的最大容量,M(草药数目)可以看作物品数目,每株药草的花费时间可以看作该物品的“重量”。于是我们可以构造一个大小为 (M + 1) * (T + 1) 的二维数组:

dp = []
for i in range(M + 1):
    dp.append([0] * (T + 1))

在dp的第0行,因为没有编号为0的物品,所以我们默认有一个物品0:花费时间0 价值0,更改前面的cost和value即可。此时,用 cost[i] 和 value[i] 即可取得编号为 i 的草药的属性

#将前面的对应代码更改成这样:
cost = [0]
value = [0]

从dp的第一行开始,按照先前推出的规则一和规则二,填充每个格子的值

for i in range(1,len(dp)):
    for j in range(len(dp[i])):
        if cost[i] < j:
            dp[i][j] = dp[i-1][j]
#规则一:当背包容量不足以装下现在正在考虑的物品时,当前格子的值和上一行相同
        else:
            dp[i][j] = max(dp[i-1][j-cost[i]] + value[i], dp[i-1][j])
#规则二:当背包容量足以放入现在正在考虑的物品时,对比放与不放两种情况,取两者之中较大的一个

最后输出 dp[-1][-1] (表格右下角的值)即可

print(dp[-1][-1])

算法优化

常数优化

在解决01背包问题的过程中,我们发现,有的物品的重量已经超过了背包的最大容量,例如《采药》给出的输入样例:

 

 显然花费时间71已经超过了规定的采药时间70,考虑这株草药是否采摘是没意义的。于是我们可以在输入数据时加入对数据是否有意义的判断,放弃记录没有意义的数据,这就是常数优化;代码如下:

T, M = map(int,input().split())
cost = [0]
value = [0]
for i in range(M):
    a, b = map(int,input().split())
    if a <= T:
        cost.append(a)
        value.append(b)
dp = []
for i in range(len(cost)):
    dp.append([0] * (T + 1))
for i in range(1,len(dp)):
    for j in range(len(dp[i])):
        if cost[i] > j:
            dp[i][j] = dp[i-1][j]
        else:
            dp[i][j] = max(dp[i-1][j-cost[i]] + value[i], dp[i-1][j])
print(dp[-1][-1])

滚动数组

当背包容量比较大,物品数目比较多时,用以上方法我们将会构造一个非常大的二维数组,占用非常多的空间。其实观察整个表格的生成过程,每一个格子的数值都只与上一行的值有关,也就是说,在考虑物品3时,关于物品1的那一行其实就可以删掉了,在考虑物品4时,关于物品2的一行以后就不会再用到了。

基于这个思想,我们可以构造一个一维数组 dp[i] ,将其中的值不断更新,这种数组被称作滚动数组;代码如下:

T, M = map(int,input().split())
cost = [0]
value = [0]
for i in range(M):
    a, b = map(int,input().split())
    if a <= T:
        cost.append(a)
        value.append(b)
dp = [0] * (T + 1)
dp_ = [0] * (T + 1)
for i in range(1,len(cost)):
    for j in range(len(dp)):
        if cost[i] > j:
            dp_[j] = dp[j]
        else:
            dp_[j] = max(dp[j-cost[i]] + value[i], dp[j])
    dp = dp_.copy()
print(dp[-1][-1])

总结

《采药》这个题例只展示了最基本的01背包的命题形式,在实际做题中,会遇到很多变式。要在这道题的基础上举一反三,才能做到“做一道题,会一类题”。

猜你喜欢

转载自blog.csdn.net/caterpie_771881/article/details/128467682
今日推荐