背包问题之模板题 Python实现

前言#

01背包——万恶之源
我一定要搞好这个背包问题!

一、 01背包#

1. 问题描述#

01背包问题:给定N个物品和容量为V的背包,每个物品有两个属性:价值wi和体积vi每个物品只能取1次,问在背包中放入哪些物品可以使得总价值最大?

输入例子:

Copy
4 5 # 物品数量和背包容量 1 2 # 物品1的体积和价值 2 4 3 4 4 5

输出例子:

Copy
8 # 价值最大的结果

2. 解题思路#

  1. 状态表示f[i, j](一般背包问题都是二维)
  • 表示所有选法,满足只从前i个物品中选,并且选出来的物品总体积小于等于j,f[i, j]的值为这些选法的价值的最大值
    • 前i个物品:这个价值最大值从前i个物品考虑,并不代表前i个物品都选了
  • 最后答案是f[N][V]
  1. 状态计算
  • 对于f[i, j],可以将该集合划分为两部分:选法包含i,选法不包含i。两部分的最大值就是f[i, j]

  • 如何计算这两部分

    • 选法不包含i:从1~i-1中选,且总体积不超过j的所有选法的最大值(这些选法一定不会包含i),即f[i-1, j]

    • 选法包含i:包含i的选法的最大值

      如何求?——“曲线救国”

      1. 先不去看第i个物品,即从第1~i-1个物品中选。

        但是由于最后要加上第i个物品,所以在第1到i-1个物品选时,选法不能超过jv[i],得出这部分最大值,即f[i1,jv[i]]

      2. 再加上第i个物品的价值,即可得到包含i的选法的最大价值

        f[i1,jv[i]]+w[i]

  • 注意,当j<vi的时候,即选法总体积装不下第i个物品的时候,选法包含i这个情况不存在

💡 总结

  1. 遍历所有物品

    ​ 遍历所有体积

  2. f[i,j]=max(f[i1,j],f[i1,jv[i]]+w[i]),其中f[i1,jv[i]]+w[i]只有当j>=v[i]的时候才存在

3. 代码实现#

3.1 朴素版#

O(NV)

Copy
f = [[0 for _ in range(N+1)] for _ in range(V+1)] for i in range(1, N+1): for j in range(1, V+1): f[i][j] = f[i-1][j] if j >= v[i]: f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]) print(f[N][V])

3.2 压缩版#

O(NV)
原理是:

  1. f[i]层只用到了f[i-1],因此可以把 i 这一维去掉
  2. 计算f[i, j]用到的j(如f[i1,j]f[i1,jv[i]])都是j,所以如果从大到小遍历所有体积,用到 j 前面的都是从上一层更新完毕的,即都为f[i-1]的
  3. f[i, j] = f[i-1, j]可以省略,其实 f[i-1, j]就是f[i]在计算之前的值,f[i, j]是f[i]计算之后的值
Copy
f = [0 for _ in range(V + 1)] for i in range(1, N + 1): for j in range(V, v[i]-1, -1): f[j] = max(f[j], f[j - v[i]] + w[i])

二、完全背包#

1. 问题描述#

在01背包问题的基础上,考虑的是每个物品可以取无限次

2. 解题思路#

  1. 状态表示f[i, j](一般背包问题都是二维)
  • 表示所有选法,满足只从前i个物品中选,并且选出来的物品总体积小于等于j,f[i, j]的值为这些选法的价值的最大值
    • 前i个物品:这个价值最大值从前i个物品考虑,并不代表前i个物品都选了
  • 最后答案是f[N][V]
  1. 状态计算
  • 对于f[i, j],可以将该集合划分为k部分:第i个物品选0个(即不包含i)、第i个物品选1个,...,第i个物品选k个(不可能无限,因为物品有体积,背包也有大小),这些部分的最大值就是f[i, j]

  • 如何计算这些部分

    • 第i个物品选k个:

      如何求?——“曲线救国”

      1. 先不去看k个第i个物品,即从第1~i-1个物品中选。

        但是由于最后要加上第i个物品,所以在第1到i-1个物品选时,选法不能超过jkv[i],得出这部分最大值,即f[i1,jkv[i]]

      2. 再加上k个第i个物品的价值,即可得到包含i的选法的最大价值

        f[i1,jkv[i]]+kw[i]

  • k的取值是0,最大值应该是k * v[i] <= j

💡 总结

遍历所有物品

​ 遍历所有体积

​ 遍历所有个数

​ 计算f[i,j]=max(f[i1,jkv[i]]+kw[i])

3. 代码实现#

3.1 朴素版#

O(NVK)

Copy
f = [[0 for _ in range(N+1)] for _ in range(V+1)] for i in range(1, N+1): for j in range(1, V+1): k = 0 while k * v[i] <= j: f[i][j] = max(f[i][j], f[i-1][j-k * v[i]] + k * w[i]) print(f[N][V])

3.2 压缩版#

O(NV)
原理:

  1. 将k展开,对比f[i, j]和f[i, j - v](v简化v[i])

image-20220613221839315

可以发现,红色部分和橙色部分相比,每一项都多了个w,因此,f[i, j]的计算可以简化成:

f[i][j]=max(f[i1][j],f[i][jv[i]]+w[i])

  1. 对比01背包的方程

    01背包:f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])

    完全背包:f[i][j]=max(f[i1][j],f[i][jv[i]]+w[i])

    可以发现,01背包是从上一层转换过来的,完全背包是从当层转移过来的,所以可以从小到大枚举所有体积,这样当算到 j 时,前面的 j-v[i] 已经被计算过了,即属于当层转移

Copy
f = [0 for _ in range(V + 1)] for i in range(1, N + 1): for j in range(v[i], V + 1): f[j] = max(f[j], f[j - v[i]] + w[i])

三、多重背包#

1. 问题描述#

在完全背包问题的基础上,考虑的是每个物品可以取Si,即会给每个物品次数的限制

2. 解题思路#

多重背包可以直接在完全背包的基础上,将k(物品取的次数)的条件加上 <= s[i] 即可

3. 代码实现#

3.1 朴素版#

O(NVS)

Copy
f = [[0 for _ in range(N+1)] for _ in range(V+1)] for i in range(1, N+1): for j in range(1, V+1): k = 0 while k <= s[i] && k * v[i] <= j: f[i][j] = max(f[i][j], f[i-1][j-k * v[i]] + k * w[i]) print(f[N][V])

4. 多重背包的优化--二进制优化#

O(NVlogS)

4.1 思路#

  1. 借鉴完全背包的思想,对比f[i, j] 和 f[i, j-v]

    image-20220613233603506

  • 可以发现,f[i, j - v]比f[i, j]的绿色部分多了f[i - 1, j - (s + 1)v] + sw,也就是说,已知红色部分的最大值,能不能推出绿色部分的最大值?

    可以将问题看成,已知红色部分的最大值和f[i - 1, j - (s + 1)v] + sw,可不可以推出绿色部分,即已知 max(a, b, c)和 c 的值,求max(a, b)。

  • 已知 max(a, b, c)和 c 的值,max(a, b)是无法确定。原因如下:

    • 如果max(a, b, c) = a/b,那么max(a, b) =a/b可以确定
    • 如果max(a, b, c) = c,那么max(a, b) = ?从已知条件是推不出的
  • 因此,不能直接用完全背包的优化来优化多重背包

  1. 二进制优化

假设s = 1023,即对于每一个物品共有0~1023种选法,将选法根据二进制打包成k组,每一组包含选取2i个物品,然后每一组最多只能取一次。这样0~1023中每一种选法都可以用k组的不同组合来表示。

k组:1, 2, 4, 8,...,512

则 考虑第一组,可以取0~1个

加上第二组,则可以取 2~3个,加起来就是取0~3个

加上第三组,则可以取 4~7个,加起来就是取0~7个

以此类推,取0~1023都可以用这些组来表示

  • 如果s不能完整分为k组,则最后一组设为C=s1k12i

    即S可以分成k组,1,2,4,...,2k1,CandC<2k

因此,我们可以将多重背包转换成01背包问题,每一组只有取或者不取

4.2 代码实现#

Copy
n, m = tuple(map(int,input().split())) # 物品总量和背包容量 v = [] w = [] for i in range(n): # 对于每一个物品 a, b, s = tuple(map(int,input().split())) k = 1 # 对k个物品进行打包 while k <= s: v.append(a * k) # 把k个物品i打包在一起 w.append(b * k) s -= k k *= 2 if s > 0: # 说明s不能被k减完,还有剩的C v.append(a * s) w.append(b * s) n = len(v) # 重新更新物品数量,为新打包的数量 # 01背包 f = [0] * (m+1) for i in range(1, n + 1): for j in range(m, v[i] - 1, -1): f[j] = max(f[j], f[j - v[i]] + w[i]) print(f[m])

四、分组背包#

1. 问题描述#

在01背包问题的基础上,考虑的是每个物品属于不同的分组,每一组只能选1个物品

image-20220614003505144

输入示例:

Copy
3 5 # 物品组数N和背包容量V # 接下来有N组数据 2 #第i组有s个物品 1 2 #第i组第j个物品的体积和价值 2 4 1 3 4 1 4 5

输出示例:

Copy
8

2. 解题思路#

每一组只能取一次,其实就有01背包的影子了:

  1. v、w设成二维的,v[i][j] 表示第i组第j个物品的体积
  2. 多一层循环遍历每一组的每一个物品,只有它的体积 <= j 时,就可以用转移方程(因此,特判的不是组别的体积,所以第二层循环的 j 从 V 到 0 遍历)

3. 代码实现#

Copy
for i in range(1,n+1): for j in range(m, -1, -1): for k in range(s): if v[i][k] <= j: f[j] = max(f[j], f[j - v[i][k]] + w[i][k])
posted @   要兵长还是里维  阅读(447)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示
CONTENTS