背包问题之01背包应用题 Python实现

1. 装箱问题

1.1 问题描述

有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积(正整数)。

要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

1.2 解题思路

这道题的问题是要使得剩余空间最小,即物品使用的空间最大,01背包问的是价值最大,可以看出,使用空间和体积有关,要01背包问题转换成总体积最大,只需将体积也看成价值,即体积=体积,价值=体积,从而套上01背包模板即可。

1.3 代码实现

m = int(input())
n = int(input())
f = [0] * (m + 1)
for i in range(n):
    v = int(input())
    for j in range(m, v - 1, -1):
        f[j] = max(f[j], f[j - v] + v)
print(m - f[m]) # 求的是剩余空间

2. 二维费用问题

2.1 收服小精灵

2.1.1 问题描述

收服精灵

对于每一个小精灵而言,需要使用很多个精灵球才能收服它,而在收服过程中,野生小精灵也会对皮卡丘造成一定的伤害(从而减少皮卡丘的体力)。

当皮卡丘的体力小于等于0时,小智就必须结束狩猎。

小智的目标有两个:主要目标是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因为他们还要继续冒险。

输入例子:

10 100 5 # V1,V2,N,分别代表小智的精灵球数量、皮卡丘初始的体力值、野生小精灵的数量
7 10 # 需要的精灵球的数量,对皮卡丘造成的伤害
2 40
2 50
1 20
4 20

输出例子:

3 30 #最多收服的小精灵数量,收服这些小精灵时皮卡丘的最多剩余体力值

2.1.2 代码思路

  1. 这道题很像01背包,因为一个精灵只能取一次。

  2. 如何判断谁是体积谁是价值?

  • 价值:所要求的最值——精灵数量
  • 体积:所花费的、有限制的——精灵球数量、体力值
  1. 如何表示二维费用?用二维的dp数组表示
  • 状态表示: f[i, j, k]表示所有只从前i个精灵中选,且精灵球数量不超过 j,体力值不超过 k 的选法的最大价值

  • 状态计算:(选或不选)\(f[i,j,k] = max(f[i-1,j,k],f[i-1,j-v1[i],k-v2[i]]+1)\)

    • j 和 k选的话都要考虑该精灵所需的体力值和精灵球数量
    • 价值为数量,所以选的话价值就+1
  1. 结果表示
  • 最多收服的小精灵数量:f[N, V1, V2-1]
    • 因为皮卡丘体力值不能等于0,所以最多也就是V2-1
  • 最多剩余力量(=最少耗费体力):在精灵球数量不变的情况下,体力从V2-1开始遍历,只要上一个(体力-1)的精灵数量和 f[N, V1, V2-1] 一样,就可以继续遍历(表示可以花更小的体力收服同样的精灵数量),直到不能再遍历,返回的值就是花费的最小体力
  1. 优化掉一维(第 i 维)

2.1.3 代码实现

V1, V2, N = tuple(map(int, input().split()))
f = [[0 for _ in range(V2 + 1)]for _ in range(V1 + 1)]
for i in range(N):
    v1, v2 = tuple(map(int, input().split()))
    for j in range(V1, v1 - 1, -1):
        for k in range(V2 - 1, v2 - 1, -1): # 注意k从V2-1开始,因为体力值最多只能用到V2-1,不能用完
            f[j][k] = max(f[j][k], f[j - v1][k - v2] + 1)
print(f[V1][V2 - 1], end = " ")
k = V2 - 1
while k > 0 and f[V1][k - 1] == f[V1][V2 - 1]:
    k -= 1
print(V2 - k) # 求的是剩余体力

2.2 潜水员

2.2.1 问题描述

潜水员

给定k个气缸,每个气缸有氧气值、氮气值、重量

要求在满足氧气 >= n,氮气 >= m的情况下所携带的气缸总重量最小值为多少

2.2.2 解题思路

  1. 一个气缸只能取一次 —— 01背包
  2. 如何判断体积和价值?
    • 体积:氧气、氮气的量 —— 不小于
    • 价值:重量 —— 最小
  3. dp分析
  • 二维费用问题,先设为f[i, j, k]来计算

  • 状态表示:f[i, j, k] 表示 所有从前i个水缸选,氧气量至少为 j, 氮气量至少为 k的所有选法的重量最小值

  • 状态计算

    划分依据:最后一个物品选还是不选(所有不含i的选法、所有包含i的选法)

    • 所有不含i的选法:f[i-1, j, k]
    • 所有包含i的选法:f[i-1, j - v1, k - v2] + w
  • 初始化:f[0, 0, 0] = 0,f[0, j, k] = 正无穷

    • f[0, 0, 0]这个状态是合法的,可以存在,但是其他的不合法,因为没有取任何水缸时氧气和氮气不可以至少为非0
  • 区别:循环的时候,j 不是从 v1 开始循环,而是从0开始循环,因为 j < v1表示 j - v1 为负数,但是这也是可行的,这表示“含量至少为负数“ 这个状态也是存在且合法的,此时这个状态和 j = 0 的状态其实是一样的(因为我们前面表示过f[0, 0, 0] = 0表示这是合法的),因此,当j - v1 为负数时,将其置为0即可,这样就不会越界,但状态也有所表示。

  1. 结果表示:从 j>= N 到 k >= M开始遍历到结尾(所需的氧气和氮气的最大值),找出f[i, j, k]的最小值

💡 总结

背包问题中有三种表达:

  1. 体积最多为 j
    • 初始化的时候全部初始为0,在算的时候体积要保证 v >= 0
  2. 体积恰好为 j
    • 初始化的时候f[0,0] = 0,其他为正无穷,在算的时候体积要保证 v >= 0
  3. 体积至少为 j
    • 初始化的时候f[0,0] = 0,其他为正无穷

2.2.3 代码实现

m, n = tuple(map(int, input().split()))
num = int(input())
M = 22 
N = 80
f = [[800010 for _ in range(N)] for _ in range(M)]
f[0][0] = 0
for i in range(num):
    a, b, c = tuple(map(int, input().split()))
    for j in range(M-1, -1, -1): # 此处是遍历到0
        for k in range(N-1, -1, -1): # 此处是遍历到0
            t1 = j - a if j - a >= 0 else 0 
            t2 = k - b if k - b >= 0 else 0
            f[j][k] = min(f[j][k], f[t1][t2] + c)
res = 800010
for i in range(m, M): # 找出最小值
    for j in range(n, N):
        res = min(res, f[i][j])
print(res)

3. 求方案数

3.1 数字组合

3.1.1 问题描述

数字组合

给定N个正整数 \(A_1,A_2,…,A_N\),从中选出若干个数,使它们的和为M,求有多少种选择方案。

输入样例:

4 4 # 整数 N M
1 1 2 2 # A1~AN

输出样例:

3

3.1.2 解题思路

  1. 一个数只能选一次,组合后他们的和恰好为M,问方案

  2. 这道题可以用01背包做:

    体积:和

    价值:方案数

    只是不再是不超过某个体积,也不是价值最大值,而是具体的值

  3. dp分析

  • 状态表示: f[i, j] 表示所有从前i个数中选,且体积恰好为j的方案个数

  • 状态计算:(选了i之后刚好为 j 和不选它刚好为 j 的方案数相加)

    \(f[i, j] = f[i - 1, j] + f[i - 1, j - a[i]]\)

    • 不再是max,而且状态转移方程不是一定要有max,而是从某个状态到某个状态。
    • 这道题是方案数,到f[i, j]时有选 i 和不选 i两种选择,但是要到f[i, j]这个状态说明和一定要恰好为j,所以这两个选择应该是:有选 i 之后刚好为 j 的方案数和不选它刚好为 j 的方案数。而这两个方案数都可以说明f[i, j]的方案数,因此相加
    • 注意:选了i之后方案数没有+1,只是说这些方案里面包含了i
  • 初始化:f[0, 0] 应该为1,其他f[0, j] = 0

    • 注意!!对于"恰好"的状态表示,都要注意初始化的时候有没有什么特殊值
  • 结果表示:f[n, m]

  1. 可以优化掉一维,从后往前遍历,然后\(f[i, j]+=f[i - 1, j - a[i]]\)即可

3.1.3 代码实现

n, m = tuple(map(int, input().split()))
a = list(map(int, input().split()))
f = [0] * (m+1)
f[0] = 1
for i in range(n):
    for j in range(m, a[i]-1, -1):
        f[j] += f[j - a[i]]
print(f[m])

3.2 背包问题求方案数

3.2.1 问题描述

题目链接

3.3 背包问题求具体方案

3.3.1 问题描述

题目链接

posted @ 2022-06-14 23:05  要兵长还是里维  阅读(586)  评论(0编辑  收藏  举报