背包问题之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 代码思路
-
这道题很像01背包,因为一个精灵只能取一次。
-
如何判断谁是体积谁是价值?
- 价值:所要求的最值——精灵数量
- 体积:所花费的、有限制的——精灵球数量、体力值
- 如何表示二维费用?用二维的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
- 结果表示
- 最多收服的小精灵数量:f[N, V1, V2-1]
- 因为皮卡丘体力值不能等于0,所以最多也就是V2-1
- 最多剩余力量(=最少耗费体力):在精灵球数量不变的情况下,体力从V2-1开始遍历,只要上一个(体力-1)的精灵数量和 f[N, V1, V2-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 解题思路
- 一个气缸只能取一次 —— 01背包
- 如何判断体积和价值?
- 体积:氧气、氮气的量 —— 不小于
- 价值:重量 —— 最小
- 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即可,这样就不会越界,但状态也有所表示。
- 结果表示:从 j>= N 到 k >= M开始遍历到结尾(所需的氧气和氮气的最大值),找出f[i, j, k]的最小值
💡 总结:
背包问题中有三种表达:
- 体积最多为 j
- 初始化的时候全部初始为0,在算的时候体积要保证 v >= 0
- 体积恰好为 j
- 初始化的时候f[0,0] = 0,其他为正无穷,在算的时候体积要保证 v >= 0
- 体积至少为 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 解题思路
-
一个数只能选一次,组合后他们的和恰好为M,问方案
-
这道题可以用01背包做:
体积:和
价值:方案数
只是不再是不超过某个体积,也不是价值最大值,而是具体的值
-
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]
- 可以优化掉一维,从后往前遍历,然后\(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])