完全背包问题 - 动态规划最优解法(Java实现)
完全背包问题 - 动态规划最优解法(Java实现)
问题描述
完全背包问题是01背包问题的扩展版本:
- 有一个容量为
capacity
的背包 - 有
n
种物品,每种物品有重量weights[i]
和价值values[i]
- 关键区别:每种物品可以选择无限次(0次、1次、2次…)
- 目标:在不超过背包容量的前提下,使背包中物品总价值最大
与01背包的核心区别
特性 | 01背包 | 完全背包 |
---|---|---|
物品使用限制 | 每个物品最多用1次 | 每个物品可用无限次 |
状态转移 | dp[i-1][w-weight] + value | dp[w-weight] + value |
关键差异 | 从上一行取值 | 从当前行已计算的值取值 |
最推荐的Java解决方案
public static int completeKnapsack(int[] weights, int[] values, int capacity) {int n = weights.length;// dp[w]表示容量为w时能获得的最大价值int[] dp = new int[capacity + 1];// 对每个容量进行计算for (int w = 1; w <= capacity; w++) {// 尝试每种物品for (int i = 0; i < n; i++) {int weight = weights[i];int value = values[i];if (weight <= w) {// 当前物品重量不超过容量,可以选择dp[w] = Math.max(dp[w], dp[w - weight] + value);}// 物品重量超过容量时,跳过(隐式else分支)}}return dp[capacity];
}
关键变量详解
变量 | 含义 | 作用 |
---|---|---|
dp[w] | 容量为w时能获得的最大价值 | 状态定义,存储子问题的最优解 |
n | 物品种类总数 | 确定内层循环的范围 |
capacity | 背包容量 | 确定dp数组大小和外层循环范围 |
weight | 当前考虑物品的重量 | 判断是否能放入当前容量的背包 |
value | 当前考虑物品的价值 | 计算选择该物品后的总价值增量 |
w | 当前考虑的背包容量 | 外层循环控制变量,逐步求解各容量下的最优解 |
i | 当前考虑的物品索引 | 内层循环控制变量,遍历所有物品种类 |
算法核心思想
关键洞察:由于每个物品可以用无限次,当我们计算 dp[w]
时,可以利用已经计算好的 dp[w-weight]
,这个值可能已经包含了当前物品的使用,因此实现了"无限次使用"的效果。
完整可视化演示
使用测试用例(精心设计以覆盖所有代码分支):
- 物品信息:[重量: [1,3,4], 价值: [15,20,30]]
- 背包容量:4
初始化阶段
DP数组初始状态:
dp[0] = 0 dp[1] = 0 dp[2] = 0 dp[3] = 0 dp[4] = 0
逐步计算过程
第1轮:计算 dp[1](容量=1)
当前容量:1
尝试物品:
物品1 (重量=1, 价值=15):
weight=1 <= w=1
✓ → 分支1:可以选择- 不选:
dp[1] = 0
- 选择:
dp[1-1] + 15 = dp[0] + 15 = 0 + 15 = 15
dp[1] = Math.max(0, 15) = 15
物品2 (重量=3, 价值=20):
weight=3 > w=1
→ 隐式else分支:跳过
物品3 (重量=4, 价值=30):
weight=4 > w=1
→ 隐式else分支:跳过
第1轮完成:dp[1] = 15
当前状态:[0, 15, 0, 0, 0]
第2轮:计算 dp[2](容量=2)
当前容量:2
尝试物品:
物品1 (重量=1, 价值=15):
weight=1 <= w=2
✓ → 分支1:可以选择- 不选:
dp[2] = 0
- 选择:
dp[2-1] + 15 = dp[1] + 15 = 15 + 15 = 30
dp[2] = Math.max(0, 30) = 30
← 选择分支胜出
物品2 (重量=3, 价值=20):
weight=3 > w=2
→ 隐式else分支:跳过
物品3 (重量=4, 价值=30):
weight=4 > w=2
→ 隐式else分支:跳过
第2轮完成:dp[2] = 30
当前状态:[0, 15, 30, 0, 0]
解释:容量2时,选择2个物品1(2×15=30)
第3轮:计算 dp[3](容量=3)
当前容量:3
尝试物品:
物品1 (重量=1, 价值=15):
weight=1 <= w=3
✓ → 分支1:可以选择- 当前:
dp[3] = 0
- 选择:
dp[3-1] + 15 = dp[2] + 15 = 30 + 15 = 45
dp[3] = Math.max(0, 45) = 45
物品2 (重量=3, 价值=20):
weight=3 <= w=3
✓ → 分支1:可以选择- 当前:
dp[3] = 45
- 选择:
dp[3-3] + 20 = dp[0] + 20 = 0 + 20 = 20
dp[3] = Math.max(45, 20) = 45
← 不选择分支胜出
物品3 (重量=4, 价值=30):
weight=4 > w=3
→ 隐式else分支:跳过
第3轮完成:dp[3] = 45
当前状态:[0, 15, 30, 45, 0]
解释:容量3时,选择3个物品1(3×15=45)比选择1个物品2(1×20=20)更优
第4轮:计算 dp[4](容量=4)
当前容量:4
尝试物品:
物品1 (重量=1, 价值=15):
weight=1 <= w=4
✓ → 分支1:可以选择- 当前:
dp[4] = 0
- 选择:
dp[4-1] + 15 = dp[3] + 15 = 45 + 15 = 60
dp[4] = Math.max(0, 60) = 60
物品2 (重量=3, 价值=20):
weight=3 <= w=4
✓ → 分支1:可以选择- 当前:
dp[4] = 60
- 选择:
dp[4-3] + 20 = dp[1] + 20 = 15 + 20 = 35
dp[4] = Math.max(60, 35) = 60
← 不选择分支胜出
物品3 (重量=4, 价值=30):
weight=4 <= w=4
✓ → 分支1:可以选择- 当前:
dp[4] = 60
- 选择:
dp[4-4] + 30 = dp[0] + 30 = 0 + 30 = 30
dp[4] = Math.max(60, 30) = 60
← 不选择分支胜出
最终完成:dp[4] = 60
最终状态:[0, 15, 30, 45, 60]
详细计算过程可视化表格
容量w | 物品1(1,15) | 物品2(3,20) | 物品3(4,30) | 最终dp[w] | 最优选择 |
---|---|---|---|---|---|
0 | - | - | - | 0 | 无 |
1 | ✓选择(0+15=15) | ✗跳过 | ✗跳过 | 15 | 1个物品1 |
2 | ✓选择(15+15=30) | ✗跳过 | ✗跳过 | 30 | 2个物品1 |
3 | ✓选择(30+15=45) | ✗不选(45>20) | ✗跳过 | 45 | 3个物品1 |
4 | ✓选择(45+15=60) | ✗不选(60>35) | ✗不选(60>30) | 60 | 4个物品1 |
代码分支覆盖分析
通过上述演示用例,我们完全覆盖了Java代码的所有分支:
分支1:weight <= w
(可以选择)
- 触发场景:物品1在所有容量下;物品2在容量3,4时;物品3在容量4时
- 代码执行:
dp[w] = Math.max(dp[w], dp[w - weight] + value)
- 说明:当前物品可以放入背包,需要比较选择和不选择哪个更优
隐式else分支:weight > w
(不能选择)
- 触发场景:物品2在容量1,2时;物品3在容量1,2,3时
- 代码行为:跳过当前物品,不执行任何操作
- 说明:当前物品太重,无法放入背包
Math.max的比较分支
选择物品更优的情况:
dp[1] = max(0, 15) = 15
:选择物品1dp[2] = max(0, 30) = 30
:选择2个物品1dp[3] = max(0, 45) = 45
:选择3个物品1dp[4] = max(0, 60) = 60
:选择4个物品1
不选择物品更优的情况:
dp[3]
考虑物品2时:max(45, 20) = 45
,保持原值dp[4]
考虑物品2时:max(60, 35) = 60
,保持原值dp[4]
考虑物品3时:max(60, 30) = 60
,保持原值
算法复杂度
- 时间复杂度:O(n × capacity),需要对每个容量尝试每种物品
- 空间复杂度:O(capacity),只需要一维dp数组
最终答案与最优方案
通过动态规划,我们得到最优解是 60,对应的最优选择方案是:
- 选择4个物品1(每个重量=1,价值=15)
- 总重量:4×1=4 ≤ 4 ✓
- 总价值:4×15=60
算法优势
这个解法具有以下优势:
- 空间优化:相比二维DP,只需要一维数组
- 逻辑清晰:外层遍历容量,内层遍历物品
- 易于理解:状态转移方程简单直观
- 高效实用:时空复杂度都是最优的
与01背包的对比总结
特性 | 01背包 | 完全背包 |
---|---|---|
问题本质 | 每个物品至多选1次 | 每个物品可选无限次 |
DP定义 | dp[i][w] :前i个物品,容量w | dp[w] :容量w的最大价值 |
状态转移 | dp[i-1][w-weight] | dp[w-weight] |
核心区别 | 从上一行取值 | 从当前行取值 |
空间复杂度 | O(n×W) | O(W) |
完全背包通过修改状态转移的取值位置,巧妙地实现了物品的重复使用,是动态规划思想的精彩体现。