一、背包问题概述
背包问题是动态规划领域的经典问题,其核心在于如何在有限容量的背包中选择物品,使得总价值最大化。根据物品选择规则的不同,主要分为两类:
- 01 背包:每件物品最多选 1 次(选或不选)。
- 完全背包:每件物品可选无限次。
本文将深入解析两者的核心逻辑、状态转移及优化技巧,并通过 Java 代码实现典型场景。
二、01 背包问题:选或不选的博弈
问题描述
给定背包容量 W
和 N
个物品(每个物品重量 w[i]
、价值 v[i]
),每个物品最多选 1 次,求背包能承载的最大价值。
核心思路
1. 状态定义
dp[j]
:背包容量为j
时能获得的最大价值。
2. 状态转移方程
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
- 选第
i
件物品:需确保容量j >= w[i]
,价值为前i-1
件物品装入容量j-w[i]
的背包价值 + 当前物品价值。 - 不选第
i
件物品:价值与前i-1
件物品装入容量j
的背包价值相同。
3. 关键实现细节
- 倒序遍历容量:从
W
到w[i]
倒序枚举,避免重复选择同一物品。 - 空间优化:使用一维数组代替二维数组,降低空间复杂度至
O(W)
。
示例分析
场景:背包容量 W=4
,物品列表 [(w=2, v=3), (w=1, v=2), (w=3, v=4)]
。
二维 DP 表演变:
容量 \ 物品 | 0(无) | 物品 1 (2,3) | 物品 2 (1,2) | 物品 3 (3,4) |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 2 | 2 |
2 | 0 | 3 | 3 | 3 |
3 | 0 | 3 | 5 | 5 |
4 | 0 | 3 | 5 | 5 |
一维优化 Java 代码:
public class Knapsack01 {
public static int solve(int[] w, int[] v, int W) {
int N = w.length;
int[] dp = new int[W + 1]; // dp[j]表示容量j的最大价值
for (int i = 0; i < N; i++) { // 遍历每个物品
for (int j = W; j >= w[i]; j--) { // 倒序遍历容量,避免重复选
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[W];
}
public static void main(String[] args) {
int[] w = {2, 1, 3};
int[] v = {3, 2, 4};
int W = 4;
System.out.println("01背包最大价值:" + solve(w, v, W)); // 输出:5
}
}
三、完全背包问题:无限选择的智慧
问题描述
与 01 背包不同,完全背包允许每件物品选无限次,求背包能承载的最大价值。
核心思路
1. 状态定义
同 01 背包,dp[j]
表示容量为 j
时的最大价值。
2. 状态转移方程
dp[j] = max(dp[j], dp[j - w[i]] + v[i])
- 允许重复选择:由于每件物品可选多次,需正序遍历容量(从
w[i]
到W
),确保当前物品可被多次选取。
3. 关键实现细节
- 正序遍历容量:从
w[i]
到W
正序枚举,允许同一件物品被多次计算。
示例分析
场景:背包容量 W=4
,物品列表同 01 背包示例(允许无限选)。
二维 DP 表演变:
容量 \ 物品 | 0(无) | 物品 1 (2,3) | 物品 2 (1,2) | 物品 3 (3,4) |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 2 | 2 |
2 | 0 | 3 | 4 | 4 |
3 | 0 | 3 | 6 | 6 |
4 | 0 | 6 | 8 | 8 |
一维优化 Java 代码:
public class KnapsackComplete {
public static int solve(int[] w, int[] v, int W) {
int N = w.length;
int[] dp = new int[W + 1];
for (int i = 0; i < N; i++) { // 遍历每个物品
for (int j = w[i]; j <= W; j++) { // 正序遍历容量,允许重复选
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[W];
}
public static void main(String[] args) {
int[] w = {2, 1, 3};
int[] v = {3, 2, 4};
int W = 4;
System.out.println("完全背包最大价值:" + solve(w, v, W)); // 输出:8(选4件物品2)
}
}
四、核心对比:01 背包 vs 完全背包
特性 | 01 背包 | 完全背包 |
---|---|---|
物品选择 | 每个物品最多选 1 次 | 每个物品可选无限次 |
容量遍历顺序 | 倒序(从 W 到 w [i]) | 正序(从 w [i] 到 W) |
状态更新逻辑 | 基于 “旧状态” 避免重复 | 基于 “新状态” 允许重复 |
时间复杂度 | O(N×W) | O(N×W) |
典型场景 | 物品限购、资源分配 | 货币兑换、原料无限供应 |
五、实战应用:LeetCode 经典题目
1. 01 背包应用:分割等和子集(LeetCode 416)
问题描述:判断数组是否可分割成两个和相等的子集。
思路:转化为 01 背包问题,目标容量为 total/2
,判断是否能恰好装满。
public class CanPartition {
public static boolean canPartition(int[] nums) {
int total = sum(nums);
if (total % 2 != 0) return false;
int target = total / 2;
boolean[] dp = new boolean[target + 1];
dp[0] = true; // 容量0时有一种方案(不选任何物品)
for (int num : nums) {
for (int j = target; j >= num; j--) { // 倒序遍历防重复
dp[j] = dp[j] || dp[j - num];
}
}
return dp[target];
}
private static int sum(int[] nums) {
return Arrays.stream(nums).sum();
}
public static void main(String[] args) {
int[] nums = {1, 5, 11, 5};
System.out.println(canPartition(nums)); // 输出:true(子集和为11)
}
}
2. 完全背包应用:零钱兑换(LeetCode 322)
问题描述:用最少硬币数组成金额 amount
,硬币可重复使用。
思路:转化为完全背包问题,目标是最小化物品数量(硬币数)。
public class CoinChange {
public static int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0; // 金额0时需0枚硬币
for (int coin : coins) {
for (int j = coin; j <= amount; j++) { // 正序遍历允许多选
if (dp[j - coin] != Integer.MAX_VALUE) {
dp[j] = Math.min(dp[j], dp[j - coin] + 1);
}
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
public static void main(String[] args) {
int[] coins = {1, 2, 5};
int amount = 11;
System.out.println(coinChange(coins, amount)); // 输出:3(5+5+1)
}
}
六、优化技巧与变种问题
1. 空间优化
- 一维数组代替二维数组,空间复杂度从
O(N×W)
降至O(W)
。
2. 多重背包转化
若物品有数量限制(如最多选 k
件),可通过二进制拆分转化为 01 背包问题。例如,最多选 3 件可拆分为 1 件、2 件两个物品。
3. 常见变种
- 恰好装满背包的方案数:初始化
dp[0] = 1
,其余为 0,通过加法原理计算方案数。 - 二维费用背包:增加一维状态(如重量和体积),状态转移为
dp[j][k] = max(...)
。
七、总结与学习建议
核心口诀
- 01 背包:倒序遍历防重复,选或不选取最值。
- 完全背包:正序遍历允重复,同物多次算价值。
练习推荐
- 01 背包:LeetCode 494. 目标和
- 完全背包:LeetCode 518. 零钱兑换 II(求方案数,需正序遍历 + 组合逻辑)
通过对比学习 01 背包与完全背包的核心逻辑,掌握动态规划的状态转移思想,可有效应对背包问题的各类变种。建议结合具体题目反复练习,加深对 “状态定义” 和 “遍历顺序” 的理解。