一、01背包
给定一个容积为V的背包,现在有N件物品,第i件物品的体积为wi,价值为vi。每件物品只能拿或者不拿,请求出体积总和不超过V的最大价值。
- 贪心策略可以吗?每次选择性价比最高的。
- V=6,三件物品,体积分别为1,3,5,价值分别为10,20,30。按照性价比肯定是优先选择体积为1的物品,然后选择体积为3,总价值为30。实际上选择体积为1、5的两件才是最优的。
给定一个容积为V的背包,现在有N件物品,第i件物品的体积为wi,价值为vi。每件物品只能拿或者不拿,请求出体积总和不超过V的最大价值。
动态规划状态:dp[i][j]:前i件物品,体积为j的最大价值。 dp[...][0]=dp[0][...]=0
对于第i件物品,仅考虑两种情况:
- 拿:dp[i][j]从dp[i-1][j-wi]转移过来
- 不拿:dp[i][j]从dp[i-1][j]转移过来
例题 P1174 小明的背包
题目描述
小明有一个容量为 V 的背包。
这天他去商场购物,商场一共有 N 件物品,第 i 件物品的体积为 wi,价值为 vi。
小明想知道在购买的物品总体积不超过 V 的情况下所能获得的最大价值为多少,请你帮他算算。
输入描述
输入第 1 行包含两个正整数 N,V,表示商场物品的数量和小明的背包容量。
第 2∼N+1行包含 2 个正整数 w,v,表示物品的体积和价值。
1≤N≤10^2,1≤V≤10^3,1≤wi,vi≤10^3。
输出描述
输出一行整数表示小明所能获得的最大价值。
N,V=map(int,input().split())
dp=[[0]*(V+1) for i in range(N+1)]
# dp[i][j]表示前i件物品,体积不超过j的最大价值
for i in range(1,N+1):
wi,vi=map(int,input().split())
for j in range(0,V+1):
if j < wi:
dp[i][j]=dp[i-1][j]
else:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi)
print(dp[N][V])
二、滚动数组优化
给定一个容积为V的背包,现在有N件物品,第i件物品的体积为wi,价值为vi。每件物品只能拿或者不拿,请求出体积总和不超过V的最大价值。
上述转移方程中,第i行的更新,仅和第i-1行有关系,因此可以使用滚动数组。
长度为2的滚动数组:
N,V=map(int,input().split())
dp=[[0]*(V+1) for i in range(2)]
#滚动数组优化
for i in range(1,N+1):
wi,vi=map(int,input().split())
for j in range(0,V+1):
if j < wi:
dp[i%2][j]=dp[(i-1)%2][j]
else:
dp[i%2][j]=max(dp[(i-1)%2][j],dp[(i-1)%2][j-wi]+vi)
print(dp[N%2][V])
使用长度为1的滚动数组优化空间:
i−1,j−w[i] ... i−1,j i,j
- 更新dp[i][j]时,用到的是上一行对应位置(dp[i - 1][j])和上一行先前位置(dp[i - 1][j - w[i]])的元素,因此可以使用单个数组进行更新:直接从大到小对dp数组进行覆盖即可。
- dp[j]=max(dp[j],dp[j−w[i]]+v[i])
- 从大到小:这样才能保证每次使用到的是上一层的值
长度为1的滚动数组:
n, V = map(int, input().split())
dp = [0] * (V+1)
for i in range(1, n + 1):
w, v = map(int, input().split())
#必须从大到小
for j in range(V, w - 1, -1):
dp[j] = max(dp[j], dp[j - w] + v)
print(dp[V])
三、例题 P2223 背包与魔法*
问题描述
小蓝面前有 N 件物品, 其中第 i 件重量是 Wi, 价值是 Vi 。她还有一个背包, 最大承重是 M 。
小蓝想知道在背包称重范围内, 她最多能装总价值多少的物品?
特别值得一提的是, 小蓝可以使用一个魔法 (总共使用一次), 将一件物品 的重量增加 K, 同时价值秝倍。(当然小蓝也可以不使用魔法)
输入格式
第一行包含 3 个整数 N、M和 K 。
以下 N 行, 每行两个整数 Wi 和 Vi 。
输出格式
一个整数代表答案。
- 和01背包基本相同,多了一个使用魔法的维度。
- 因此状态增加一维,dp[i][j][k]表示前i件物品,重量为j,使用魔法次数为k次的最大价值。
- 对于第i件物品,存在三种选项:选、不选、选并且使用魔法。
- dp[i][j][k]=max(dp[i - 1][j][k], dp[i - 1][j - w[i]][k]+v[i])
- or k == 1: dp[i - 1][j - w[i] - K][0]+2v[i] 。
N, M, K = map(int, input().split())
# 滚动数组dp[i][j][k]表示前i件物品,重量为j,魔法次数为k的最大价值
# dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - w][k] + v)
# if k == 1: dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - w - K][0] + 2 * v)
dp = [[[0] * 2 for j in range(M + 1)] for i in range(N + 1)]
for i in range(1, N + 1):
w, v = map(int, input().split())
for j in range(M + 1):
for k in range(2):
if j < w:
dp[i][j][k] = dp[i-1][j][k]
else:
dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-w][k]+v)
if k == 1 and j >= w + K:
dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-w-K][0]+2*v)
# print(dp[i])
print(max(dp[N][M][0], dp[N][M][1]))
N, M, K = map(int, input().split())
# 滚动数组dp[j][k]表示前i件物品, 重量为j, 魔法次数为k的最大价值
# dp[j][k] = max(dp[j][k], dp[j - w][k] + v)
# if k == 1: dp[j][k] = max(dp[j][k], dp[j - w - K][0] + 2 * v)
dp = [[0] * 2 for j in range(M + 1)]
for i in range(1, N + 1):
w, v = map(int, input().split())
for j in range(M, w - 1, -1):
dp[j][0] = max(dp[j][0], dp[j - w][0] + v)
dp[j][1] = max(dp[j][1], dp[j - w][1] + v)
if j >= w + K:
dp[j][1] = max(dp[j][1], dp[j - w - K][0] + 2 * v)
# print(dp)
print(max(dp[M][0], dp[M][1]))
特性 | 代码1:三维数组 | 代码2:二维滚动数组 |
---|---|---|
空间复杂度 | 较高:O(N * M * 2) ,因为使用了三维数组存储所有状态。 | 较低:O(M * 2) ,使用二维数组滚动更新,节省空间。 |
时间复杂度 | 较高:O(N * M * 2) ,因为需要遍历三维数组。 | 相同:O(N * M * 2) ,但实际运行效率更高,因为减少了内存访问开销。 |
更新方式 | 每次更新时,直接覆盖dp[i][j][k] 的值。 | 从大到小更新dp[j][k] ,避免覆盖未使用的上一行数据。 |
代码复杂度 | 较高:需要显式处理i 维度的更新逻辑。 | 较低:通过从大到小更新,隐式处理了i 维度的滚动逻辑。 |
适用场景 | 适合初学者理解动态规划的状态转移过程。 | 适合优化空间复杂度,适合大规模数据场景。 |
输出结果 | 结果存储在dp[N][M][0] 和dp[N][M][1] 中,取最大值。 | 结果存储在dp[M][0] 和dp[M][1] 中,取最大值。 |