题目描述
若干货物的信息记录于二维数组 goods 中,其中 goods[i] = [weight,value],表示第 i 个货物的价值为 value,重量为 weight。从中选取一些货物,求当选取的货物总重量不超过 maxWeight 时的货物最大价值总和。
示例:
输入:
goods = [[7,7],[5,1],[8,9],[5,7],[6,8]], maxWeight = 21
输出: 24
解释:
选取第 2、3、4个货物(下标从 0 开始),此时商品重量总和为 8 + 5 + 6 = 19,商品价值总和为 9 + 7 + 8 = 24。
提示:
- 1 <= goods.length <= 100
- 1 <= goods[i][0] <= 10^5
- 1 <= goods[i][1] <= 100
- 1 <= maxWeight <= 10^7
问题分析
这是一个典型的0-1背包问题,每个货物只能选择拿或不拿,不能部分选取。
问题特征:
- 物品有限:总共有 n 个货物
- 容量限制:背包最大承重为 maxWeight
- 每个物品有两个属性:重量和价值
- 目标:在不超过背包容量的前提下,最大化物品的总价值
决策过程:
对于每个货物,我们面临两个选择:
- 选择该货物:获得其价值,但消耗背包容量
- 不选择该货物:不获得价值,也不消耗容量
解题思路
动态规划方法
状态定义:
- dp[i][w] 表示考虑前 i 个货物,背包容量为 w 时能获得的最大价值
状态转移方程:
dp[i][w] = max(
dp[i-1][w], // 不选择第i个货物
dp[i-1][w-weight[i]] + value[i] // 选择第i个货物(如果容量足够)
)
边界条件:
- dp[0][w] = 0(没有货物时,价值为0)
- dp[i][0] = 0(背包容量为0时,价值为0)
算法过程
以示例 goods = [[7,7],[5,1],[8,9],[5,7],[6,8]], maxWeight = 21 为例:
动态规划表格构建过程
货物信息:
索引 重量 价值
0 7 7
1 5 1
2 8 9
3 5 7
4 6 8
构建dp表格(部分关键状态):
i\w | 0 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 18 | 19 | 21 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 | 7 |
2 | 0 | 1 | 1 | 7 | 7 | 8 | 8 | 8 | 8 | 8 | 8 | 8 |
3 | 0 | 1 | 1 | 7 | 9 | 9 | 9 | 9 | 16 | 16 | 16 | 17 |
4 | 0 | 7 | 7 | 8 | 9 | 14 | 16 | 16 | 16 | 16 | 23 | 24 |
5 | 0 | 7 | 8 | 8 | 9 | 14 | 16 | 17 | 17 | 22 | 23 | 24 |
最终答案: dp[5][21] = 24
最优解构成
选择的货物:
- 货物2:重量8,价值9
- 货物3:重量5,价值7
- 货物4:重量6,价值8
- 总重量:8 + 5 + 6 = 19 ≤ 21 ✓
- 总价值:9 + 7 + 8 = 24
算法实现
动态规划
Java版本
/**
* 2. 选取货物
* 动态规划
*/
class Solution {
public int maximumGoodsValue(int[][] goods, int maxWeight) {
int n = goods.length;
// dp[i][w] 表示考虑前i个货物,背包容量为w时的最大价值
int[][] dp = new int[n + 1][maxWeight+1];
for (int i = 1; i <= n; i++) {
int weight = goods[i-1][0]; // 当前货物重量
int value = goods[i-1][1]; // 当前货物价值
// 遍历每个可能的背包容量
for (int w = 0; w <= maxWeight; w++) {
// 不选择当前货物
dp[i][w] = dp[i-1][w];
// 如果背包容量足够,考虑选择当前货物
if (w >= weight) {
dp[i][w] = Math.max(dp[i][w], dp[i-1][w-weight] + value);
}
}
}
return dp[n][maxWeight];
}
}
C# 版本
/**
* 2. 选取货物
* 动态规划
*/
public class Solution
{
public int MaximumGoodsValue(int[][] goods, int maxWeight)
{
int n = goods.Length;
int[,] dp = new int[n + 1, maxWeight + 1];
for (int i = 1; i <= n; i++)
{
int weight = goods[i - 1][0];
int value = goods[i - 1][1];
for (int w = 0; w <= maxWeight; w++)
{
dp[i, w] = dp[i - 1, w];
if (w >= weight)
{
dp[i, w] = Math.Max(dp[i, w], dp[i - 1, w - weight] + value);
}
}
}
return dp[n, maxWeight];
}
}
空间优化
Java版本
/**
* 2. 选取货物
* 动态规划,空间优化
*/
class Solution {
public int maximumGoodsValue(int[][] goods, int maxWeight) {
int n = goods.length;
int[] dp = new int[maxWeight + 1];
for (int i = 1; i <= n; i++) {
int weight = goods[i - 1][0];
int value = goods[i - 1][1];
// 从后往前遍历,避免重复计算
for (int w = maxWeight; w >= weight; w--) {
dp[w] = Math.max(dp[w], dp[w - weight] + value);
}
}
return dp[maxWeight];
}
}
C# 版本
/**
* 2. 选取货物
* 动态规划,空间优化
*/
public class Solution
{
public int MaximumGoodsValue(int[][] goods, int maxWeight)
{
int n = goods.Length;
int[] dp = new int [maxWeight + 1];
for (int i = 1; i <= n; i++)
{
int weight = goods[i - 1][0];
int value = goods[i - 1][1];
for (int w = maxWeight; w >= weight; w--)
{
dp[w] = Math.Max(dp[w], dp[w - weight] + value);
}
}
return dp[maxWeight];
}
}
复杂度分析
时间复杂度
- 二维DP版本:O(n × maxWeight)
- 需要填充 n × maxWeight 大小的表格
- 一维优化版本:O(n × maxWeight)
- 虽然空间优化了,但时间复杂度不变
空间复杂度
- 二维DP版本:O(n × maxWeight)
- 一维优化版本:O(maxWeight)
进一步优化思路:状态转换
核心洞察
观察到物品价值范围很小(最大100),而背包容量很大(最大10^7),我们可以转换DP的状态定义:
传统思路:
- 状态:dp[w] = 容量为w时的最大价值
- 转移:枚举容量,更新价值
优化思路:
- 状态:dp[v] = 达到价值v所需的最小重量
- 转移:枚举价值,更新重量
优化后的复杂度
- 最大总价值:100(物品数) × 100(单价值) = 10,000
- 时间复杂度:O(n × V) = O(100 × 10,000) = O(10^6) ,大幅优化
- 空间复杂度:O(V) = O(10,000) ,显著减少
算法详解
状态转移过程
让我们通过示例来理解算法过程:
输入: goods = [[7,7],[5,1],[8,9],[5,7],[6,8]], maxWeight = 21
初始化
maxTotalValue = 5 × 100 = 500
dp = [0, ∞, ∞, ∞, ..., ∞] // 长度为501
处理物品0:[7,7](重量7,价值7)
从 v=500 到 v=7:
如果 dp[v-7] != ∞,则 dp[v] = min(dp[v], dp[v-7] + 7)
v=7: dp[7] = min(∞, dp[0] + 7) = min(∞, 0 + 7) = 7
结果:dp = [0, ∞, ∞, ∞, ∞, ∞, ∞, 7, ∞, ...]
含义:价值7需要重量7
处理物品1:[5,1](重量5,价值1)
v=8: dp[8] = min(∞, dp[7] + 5) = min(∞, 7 + 5) = 12
v=1: dp[1] = min(∞, dp[0] + 5) = min(∞, 0 + 5) = 5
结果:dp = [0, 5, ∞, ∞, ∞, ∞, ∞, 7, 12, ∞, ...]
含义:价值1需要重量5,价值8需要重量12
继续处理剩余物品...
最终找到在重量限制21内能达到的最大价值。
关键思想解释
- 状态定义转换:
- 传统:dp[重量] = 最大价值
- 优化:dp[价值] = 最小重量
- 为什么逆序遍历:
- 保证每个物品只被使用一次
- 避免在同一轮更新中重复使用同一物品
- 为什么这样优化有效:
- 价值范围小(最大10,000)
- 重量范围大(最大10^7)
- 通过转换状态,将复杂度从O(重量)降到O(价值)
算法实现
Java 版本
import java.util.Arrays;
/**
* 2. 选取货物
* 按价值DP
*/
class Solution {
public int maximumGoodsValue(int[][] goods, int maxWeight) {
int n = goods.length;
// 计算最大可能的总价值
int maxTotalValue = n * 100;
// dp[v] 表示达到价值v所需的最小重量
// 初始化为无穷大,表示无法达到该价值
int[] dp = new int[maxTotalValue + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
// 价值为0时,重量也为0
dp[0] = 0;
for (int i = 1; i <= n; i++) {
int weight = goods[i - 1][0]; // 当前货物重量
int value = goods[i - 1][1]; // 当前货物价值
// 从高价值向低价值遍历,避免重复使用同一物品
for (int v = maxTotalValue; v >= value; v--) {
// 如果可以达到价值 v-value,则可以通过添加当前物品达到价值v
if (dp[v - value] != Integer.MAX_VALUE) {
dp[v] = Math.min(dp[v], dp[v - value] + weight);
}
}
}
// 找到最大价值,使其对应的重量不超过maxWeight
int res = 0;
for (int v = maxTotalValue; v >= 0; v--) {
if (dp[v] <= maxWeight) {
res = v;
break;
}
}
return res;
}
}
C# 版本
/**
* 2. 选取货物
* 按价值DP
*/
public class Solution
{
public int MaximumGoodsValue(int[][] goods, int maxWeight)
{
int n = goods.Length;
// 计算最大可能的总价值
int maxTotalValue = 100 * n;
// dp[v] 表示达到价值v所需的最小重量
// 初始化为无穷大,表示无法达到该价值
int[] dp = new int[maxTotalValue + 1];
Array.Fill(dp, int.MaxValue);
// 价值为0时,重量也为0
dp[0] = 0;
for (int i = 0; i < n; i++)
{
int weight = goods[i][0];
int value = goods[i][1];
// 从高价值向低价值遍历,避免重复使用同一物品
for (int v = maxTotalValue; v >= value; v--)
{
// 如果可以达到价值 v-value,则可以通过添加当前物品达到价值v
if (dp[v - value] != int.MaxValue)
{
dp[v] = Math.Min(dp[v], dp[v - value] + weight);
}
}
}
// 找到最大价值,使其对应的重量不超过maxWeight
int res = 0;
for (int v = 0; v <= maxTotalValue; v++)
{
if (dp[v] <= maxWeight)
{
res = v;
}
}
return res;
}
}
复杂度分析
- 时间复杂度:O(n × V) = O(100 × 10,000) = O(10^6)
- 空间复杂度:O(V) = O(10,000)
相比传统方法的O(10^9)时间复杂度,这是一个巨大的优化!