0/1背包问题是动态规划(DP)领域的经典问题,也是理解“状态转移”和“空间优化”的绝佳案例,看似简单——给定物品和背包容量,选择物品装入背包使总价值最大(每个物品只能选一次),但其中蕴含的DP设计思想可推广到大量组合优化问题。
给定n
个物品,每个物品有两个属性:重量w[i]
和价值v[i]
;有一个容量为C
的背包。每个物品只能选择装入或不装入(0/1选择),求在背包不超过容量C
的前提下,能装入物品的最大总价值。
示例:
n=3, C=4, w=[2,1,3], v=[4,2,3]
6
(选择第0个和第1个物品,总重量2+1=3≤4,总价值4+2=6)0/1背包的本质是带约束的组合优化问题,需在“选与不选”的决策中找到最优解,适合用动态规划求解。
动态规划的核心是“定义子问题并找到递推关系”。
定义状态:
设dp[i][j]
表示“考虑前i
个物品(下标0~i-1),背包容量为j
时的最大价值”。
(注:i
表示“物品数量”,j
表示“当前背包容量”,子问题需同时约束这两个维度)
递推关系:
对于第i-1
个物品(当前考虑的物品),有两种选择:
i-1
个物品在容量j
下的最大价值 → dp[i][j] = dp[i-1][j]
。j ≥ w[i-1]
):总价值 = 前i-1
个物品在容量j-w[i-1]
下的价值 + 当前物品价值 → dp[i][j] = dp[i-1][j - w[i-1]] + v[i-1]
。dp[i][j] = max(dp[i-1][j], (j >= w[i-1] ? dp[i-1][j - w[i-1]] + v[i-1] : 0))
边界条件:
i=0
(无物品):dp[0][j] = 0
(无论容量多大,价值都是0)。j=0
(容量为0):dp[i][0] = 0
(无法装入任何物品)。public class ZeroOneKnapsack {
public int maxValue(int n, int C, int[] w, int[] v) {
// 二维DP数组:dp[i][j] = 前i个物品,容量j时的最大价值
int[][] dp = new int[n + 1][C + 1];
// 填充DP表:从1个物品开始遍历
for (int i = 1; i <= n; i++) {
// 当前物品的重量和价值(i-1对应原数组下标)
int currW = w[i - 1];
int currV = v[i - 1];
// 遍历所有可能的容量
for (int j = 1; j <= C; j++) {
// 情况1:不装入当前物品
int notTake = dp[i - 1][j];
// 情况2:装入当前物品(需满足容量约束)
int take = (j >= currW) ? (dp[i - 1][j - currW] + currV) : 0;
dp[i][j] = Math.max(notTake, take);
}
}
return dp[n][C]; // 考虑所有物品,容量为C时的最大价值
}
public static void main(String[] args) {
ZeroOneKnapsack solution = new ZeroOneKnapsack();
int n = 3; // 3个物品
int C = 4; // 背包容量4
int[] w = {2, 1, 3}; // 物品重量
int[] v = {4, 2, 3}; // 物品价值
System.out.println(solution.maxValue(n, C, w, v)); // 输出6
}
}
n
个物品,内层循环遍历C
种容量,总操作次数为 n × C n \times C n×C。(n+1) × (C+1)
个状态。适用场景:背包容量C
较小(如C ≤ 1000
)时,二维DP简单直观,易于理解和实现。
二维DP的空间复杂度可优化至 O ( C ) O(C) O(C),核心是利用“状态依赖关系”删除冗余维度。
观察二维DP的递推关系:dp[i][j]
仅依赖dp[i-1][j]
(上一行同列)和dp[i-1][j - w[i-1]]
(上一行左侧列)。这意味着:
dp[j]
,代表“当前行(第i个物品)容量j时的最大价值”。若直接复用一维数组并按正序遍历容量j
,会导致“当前物品被多次装入”(违背0/1背包“每个物品只能选一次”的约束)。例如:
j
时,计算dp[j]
用到的dp[j - w[i-1]]
可能已被更新(属于当前物品的状态),相当于多次装入。解决方案:逆序遍历容量j
(从C
到w[i-1]
)。
dp[j]
时,dp[j - w[i-1]]
仍是“上一行”的状态(未被当前物品更新),符合0/1背包的约束。public class ZeroOneKnapsackOptimized {
public int maxValue(int n, int C, int[] w, int[] v) {
// 一维DP数组:dp[j] = 容量j时的最大价值(滚动更新)
int[] dp = new int[C + 1];
for (int i = 0; i < n; i++) { // 遍历每个物品
int currW = w[i];
int currV = v[i];
// 逆序遍历容量(避免当前物品被多次选择)
for (int j = C; j >= currW; j--) {
// 不装入:dp[j](上一行状态);装入:dp[j - currW] + currV
dp[j] = Math.max(dp[j], dp[j - currW] + currV);
}
}
return dp[C];
}
public static void main(String[] args) {
ZeroOneKnapsackOptimized solution = new ZeroOneKnapsackOptimized();
int n = 3;
int C = 4;
int[] w = {2, 1, 3};
int[] v = {4, 2, 3};
System.out.println(solution.maxValue(n, C, w, v)); // 输出6
}
}
以示例数据为例,一维dp
数组的更新过程如下:
dp = [0,0,0,0,0]
(容量0~4)j=4→2
:
j=4
:dp[4] = max(0, dp[2]+4) = 4
j=3
:dp[3] = max(0, dp[1]+4) = 0
(dp[1]=0)j=2
:dp[2] = max(0, dp[0]+4) = 4
dp = [0,0,4,0,4]
j=4→1
:
j=4
:max(4, dp[3]+2)=4
(dp[3]=0)j=3
:max(0, dp[2]+2)=6
(dp[2]=4)j=2
:max(4, dp[1]+2)=4
j=1
:max(0, dp[0]+2)=2
dp = [0,2,4,6,4]
j=4→3
:
j=4
:max(4, dp[1]+3)=max(4,5)=5
(dp[1]=2)j=3
:max(6, dp[0]+3)=6
dp = [0,2,4,6,5]
,dp[4]=5
?不,示例输出应为6——注意:最终结果取dp[C]
,但此时dp[3]=6
(容量3时价值6),而背包容量4允许装容量3的物品,因此最大价值为6。适用场景:背包容量C
较大(如C ≤ 10^4
)时,空间优化可显著减少内存占用,避免内存溢出。
问题:要求背包恰好装满,求最大价值(若无法装满,返回-1或其他标记)。
解法:
dp[j]
为-∞
(表示无法装满),仅dp[0] = 0
(容量0恰好装满,价值0)。dp[j - currW]
不是-∞
(即j - currW
可装满)时,才考虑装入当前物品。public int maxValueExactlyFull(int n, int C, int[] w, int[] v) {
int[] dp = new int[C + 1];
Arrays.fill(dp, Integer.MIN_VALUE);
dp[0] = 0; // 容量0恰好装满
for (int i = 0; i < n; i++) {
int currW = w[i];
int currV = v[i];
for (int j = C; j >= currW; j--) {
if (dp[j - currW] != Integer.MIN_VALUE) {
dp[j] = Math.max(dp[j], dp[j - currW] + currV);
}
}
}
return dp[C] == Integer.MIN_VALUE ? -1 : dp[C];
}
问题:求恰好装满背包的方案总数(每个物品只能用一次)。
解法:
dp[0] = 1
(容量0有1种方案:不装任何物品),其他dp[j] = 0
。dp[j] += dp[j - currW]
(装入当前物品的方案数 = 不装的方案数 + 装的方案数)。public int countWays(int n, int C, int[] w) {
int[] dp = new int[C + 1];
dp[0] = 1; // 初始状态:容量0有1种方案
for (int i = 0; i < n; i++) {
int currW = w[i];
for (int j = C; j >= currW; j--) {
dp[j] += dp[j - currW];
}
}
return dp[C];
}
问题:物品有重量w[i]
和体积s[i]
两个约束,背包有最大重量C
和最大体积S
,求最大价值。
解法:
dp[j][k]
(j
为重量,k
为体积)。dp[j][k] = max(dp[j][k], dp[j - w[i]][k - s[i]] + v[i])
。总结与最佳实践
0/1背包是动态规划的入门经典,核心要点在于:
- 状态设计:如何用“维度”刻画子问题(物品数+容量)。
- 空间优化:利用状态依赖关系压缩空间(二维→一维,逆序遍历)。
- 变种迁移:同一模型可解决不同场景的组合优化问题。
That’s all, thanks for reading~~
觉得有用就点个赞
、收进收藏
夹吧!关注
我,获取更多干货~