问题描述
一个旅行者外出旅行时需要将 n 件物品装入背包,背包的总容量为 m。每个物品都有一个重量和一个价值。你需要根据这些物品的重量和价值,决定如何选择物品放入背包,使得在不超过总容量的情况下,背包中物品的总价值最大。
给定两个整数数组 weights 和 values,其中 weights[i] 表示第 i 个物品的重量,values[i] 表示第 i 个物品的价值。你需要输出在满足背包总容量为 m 的情况下,背包中物品的最大总价值。
题目分析
本题是经典的0/1 背包问题,在算法竞赛、面试和日常工程中非常常见。其特点是选择性和容量限制,在有限资源下追求最大收益。问题描述如下:
- 旅行者有 件物品,每件物品有重量和价值;
- 背包的容量为 ,不能超过此重量;
- 需要选择物品,使得装入背包的物品总价值最大。
我们需要编写一个算法,接受输入 、、 和 ,输出能获得的最大总价值。
解题思路
这个问题的本质是一个动态规划问题。通过使用状态定义、状态转移方程、初始化条件,我们可以将问题转化为求解一个最优值。
1. 动态规划简介
动态规划是一种通过分解问题并保存子问题解的方法,用于解决最优化问题。对于背包问题,每件物品只有选或不选两种情况,而背包有一个容量限制。因此,我们需要根据前面选择的物品状态,推导当前的最优解。
2. 状态定义
我们用一个一维数组 dp[j] 表示:
当背包容量为 时,能获得的最大总价值。
这里的容量 是从 到 ,表示背包在不同容量下的最优状态。
3. 状态转移方程
对于每个物品 ,我们有两种选择:
- 不选择物品 : 背包的最大价值与前面的状态相同,即
dp[j]不变; - 选择物品 : 在选择 的情况下,背包需要减去物品 的重量,同时增加物品 的价值,即
dp[j - weights[i]] + values[i]。
因此,状态转移方程为:
这个公式表示,对于当前背包容量 ,我们选择是否加入第 件物品,以保证总价值最大。
4. 遍历顺序
- 外层循环: 遍历物品 ;
- 内层循环: 遍历背包容量 ,从 到 逆序更新。
为什么逆序?因为我们希望当前状态dp[j]不被覆盖,仍能使用上一轮的状态dp[j - weights[i]]。
5. 初始化
- 对于容量为 的背包,无论放什么物品,总价值都是 ,所以
dp[0] = 0。 - 其他容量 的初始值也可以设为 ,表示当前没有物品装入背包。
6. 返回结果
最终,dp[m] 表示在背包容量为 时,能装入的最大总价值。
代码实现
以下是完整代码实现:
def solution(n: int, weights: list, values: list, m: int) -> int:
# 初始化 DP 数组
dp = [0] * (m + 1)
# 遍历每个物品
for i in range(n):
# 逆序遍历容量
for j in range(m, weights[i] - 1, -1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
# 返回最大总价值
return dp[m]
代码详解
1. 初始化 DP 数组
我们使用一个长度为 的数组 dp 表示不同容量下的最大总价值:
dp = [0] * (m + 1)
初始时,dp[j] = 0,因为还没有装入物品。
2. 遍历每个物品
对于每件物品 ,我们需要检查它是否能够放入当前容量 的背包中:
for i in range(n): # 遍历每个物品
3. 逆序遍历背包容量
为了防止当前轮次更新覆盖之前的状态,我们从最大容量 开始遍历:
for j in range(m, weights[i] - 1, -1): # 逆序遍历
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
dp[j]表示不选第 件物品;dp[j - weights[i]] + values[i]表示选择第 件物品。
两者取最大值即可。
复杂度分析
1. 时间复杂度
- 外层循环 次,表示遍历每个物品;
- 内层循环 次,表示遍历背包容量;
- 总时间复杂度为 。
2. 空间复杂度
- 只需要一个长度为 的数组
dp,空间复杂度为 。
优化方向
-
空间优化: 如果需要记录选择的物品,可以用二维数组
dp[i][j],表示前 个物品的状态。 -
变体问题:
- 完全背包:每件物品可以选择多次;
- 多重背包:每件物品有数量限制。
总结
动态规划通过将问题分解为子问题并利用其结果,显著减少了计算量。对于本题,核心在于明确状态定义和推导转移方程。通过一维数组优化,空间复杂度降为 ,非常高效!