背包问题之模板题 Python实现

前言

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

一、 01背包

1. 问题描述

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

输入例子:

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

输出例子:

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个物品选时,选法不能超过\(j - v[i]\),得出这部分最大值,即\(f[i-1, j - v[i]]\)

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

        \(f[i-1, j - v[i]] + w[i]\)

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

💡 总结

  1. 遍历所有物品

    ​ 遍历所有体积

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

3. 代码实现

3.1 朴素版

\(O(NV)\)

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[i-1, j],f[i-1, j-v[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]计算之后的值
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个物品选时,选法不能超过\(j - k * v[i]\),得出这部分最大值,即\(f[i-1, j - k* v[i]]\)

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

        \(f[i-1, j - k * v[i]] + k * w[i]\)

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

💡 总结

遍历所有物品

​ 遍历所有体积

​ 遍历所有个数

​ 计算\(f[i,j]= max(f[i-1, j - k * v[i]] + k * w[i])\)

3. 代码实现

3.1 朴素版

\(O(NVK)\)

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[i-1][j], f[i][j-v[i]]+w[i])\)

  1. 对比01背包的方程

    01背包:\(f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])\)

    完全背包:\(f[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i])\)

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

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. 问题描述

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

2. 解题思路

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

3. 代码实现

3.1 朴素版

\(O(NVS)\)

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组,每一组包含选取\(2^i\)个物品,然后每一组最多只能取一次。这样0~1023中每一种选法都可以用k组的不同组合来表示。

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

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

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

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

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

  • 如果s不能完整分为k组,则最后一组设为\(C = s - \sum_1^{k-1} 2^i\)

    即S可以分成k组,\(1, 2, 4, ..., 2^{k-1},C \quad and \quad C < 2^{k}\)

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

4.2 代码实现

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

输入示例:

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

输出示例:

8

2. 解题思路

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

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

3. 代码实现

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 @ 2022-06-14 00:47  要兵长还是里维  阅读(385)  评论(0编辑  收藏  举报