在背包问题中,遍历容量顺序的不同之所以会影响是否重复选择同一物品,本质是因为动态规划的状态覆盖机制。虽然外层循环遍历的是物品,但内层循环的容量遍历方向会直接影响状态转移时的依赖关系。以下分步骤解释:
1. 核心逻辑:外层遍历物品,内层遍历容量
-
0-1背包(每个物品选一次):外层遍历物品,内层逆序遍历容量。
-
完全背包(允许重复选):外层遍历物品,内层顺序遍历容量。
关键区别:
-
逆序遍历容量时,当前物品的状态更新不会影响后续容量的计算(避免重复选择)。
-
顺序遍历容量时,当前物品的状态更新会立即被后续容量利用(允许多次选择)。
2. 直观例子:0-1背包 vs 完全背包
假设物品重量 w=2
,价值 v=5
,背包容量 C=4
,初始状态 dp = [0, 0, 0, 0, 0]
。
情况1:0-1背包(逆序遍历容量)
-
外层循环:处理当前物品(假设是第一个物品)。
-
内层循环:从
j=4
到j=2
(逆序)。-
j=4
:dp[4] = max(dp[4], dp[4-2] + 5) = max(0, dp[2] + 5)
此时dp[2]
尚未被更新(仍是初始值0),所以dp[4] = 5
。 -
j=2
:dp[2] = max(dp[2], dp[0] + 5) = 5
。
-
结果:dp[4] = 5
(只选了一次当前物品)。
关键点:逆序保证了计算大容量时,小容量的状态尚未被当前物品污染。
情况2:完全背包(顺序遍历容量)
-
外层循环:处理当前物品。
-
内层循环:从
j=2
到j=4
(顺序)。-
j=2
:dp[2] = max(dp[2], dp[0] + 5) = 5
。 -
j=4
:dp[4] = max(dp[4], dp[2] + 5) = 5 + 5 = 10
。
-
结果:dp[4] = 10
(选了两次当前物品)。
关键点:顺序遍历时,小容量状态已被更新,大容量可以复用更新后的值。
3. 为什么容量遍历顺序会影响物品选择次数?
-
逆序遍历容量:
在计算dp[j]
时,dp[j - w]
的值来自上一轮外层循环(即未选当前物品时的状态)。
→ 每个物品只能在每个容量下被选一次。 -
顺序遍历容量:
在计算dp[j]
时,dp[j - w]
的值可能已经被当前外层循环更新过(即已选过当前物品)。
→ 允许在更大容量中重复选择当前物品。
4. 二维数组 vs 一维数组的对比
-
二维数组(无覆盖问题):
状态定义为dp[i][j]
,表示前i
个物品在容量j
下的最大价值。
无论顺序还是逆序遍历容量,都不会影响状态覆盖,因为每个物品的状态独立存储。 -
一维数组(空间优化):
状态压缩为dp[j]
,复用同一数组。此时遍历顺序必须控制状态覆盖的时机:-
逆序:保证计算
dp[j]
时,dp[j - w]
是上一轮的值(未选当前物品)。 -
顺序:允许
dp[j - w]
是当前轮的值(已选当前物品)。
-
总结:遍历顺序的本质
-
遍历物品:决定“是否考虑当前物品”。
-
遍历容量顺序:决定“如何利用已计算的状态”。
-
逆序 → 确保每个物品只被考虑一次(0-1背包)。
-
顺序 → 允许同一物品被多次使用(完全背包)。
-