"你的背包,让我走得好缓慢"
上文重识了 0-1背包问题,本文开始 完全背包 问题。
完全背包
问题形态
问题描述: 有 N 件物品和一个最多能承受重量为 W(W=4) 的背包,第 i 件物品的重量为 weights[i],价值为 values[i],每件物品可使用无限次。求解该背包承载物品的最大价值为多少?
i012weights134values152030
问题分析
对于 0−1 背包,相同物品只能取 0 次或 1 次,而对于完全背包,相同物品可以取任意次(次数 ≥0)。
完全背包还有一个特殊的地方就是,选取物品时是否存在排列与组合的关系。举个例子,从集合 a, b, c 中选取,
-
对于组合 (a,a,b) 和 (a,b,a) 是完全一样的排列,因为 组合 强调整体,不强调顺序;
-
对于排列 (a,a,b) 和 (a,b,a) 是两种不同的排列,因为 排列 既强调整体,又强调顺序。
具体场景详解文末题解部分。
1、确定 dp 数组及含义
💥dp[i][j] 代表从 [0,i] 中取任意物品,放进容量为 j 的背包后的最大价值,其中 i∈[0,N),j∈[0,W](纵轴表示物品序号,横轴表示背包容量大小)。
dp[i,j]01201234
2、确定 dp 状态方程
对于第 i 件物品,有两种状态,即放入背包或不放入背包。
主动不放入:主动放弃第 i 个物品,并不受限于背包的容量,即,dp[i][j]=dp[i−1][j]
被动不放入:这种情况下受限于背包的容量,不能再将 weights[i] 的物品放入背包,此时 j<k×wights[i],即 dp[i][j]=dp[i−1][j],其中 k≥1,代表当前物品所取的个数
主动放入:那么当前背包容量一定存在 j≥k×wights[i],而且要先从背包里腾出这个空间,才能将重量为 weights[i],价值为 values[i] 的物品放入背包,即,dp[i][j]=dp[i−1][j−k×weights[i]]+k×values[i]
综上所述,令 z=j−k×weights[i]
dp[i][j]={dp[i−1][j]max(dp[i−1][j],dp[i−1][z]+k×values[i])z<0z≥0
3、确定 dp 初始状态
首先,如果背包的容量为 0,所承载的最大价值必定为 0,dp[i][0]=0,即
dp[i,j]01200001234
其次,只有第一件物品时(weights[0]=1,values[0]=15),不同容量的背包所承载的最大价值为,dp[0][j]=floor(j/weights[0])×values[0] (尽可能装入物品),即
dp[i,j]0120000115230345460
递推说明:
-
💥背包容量为 2 时,选择两件 weight[0],此时的总价值为 2×value[0]=30
-
💥背包容量为 3 时,选择 3 件 weight[0],此时的总价值为 3×value[0]=45
-
💥背包容量为 4 时,选择 4 件 weight[0],此时的总价值为 4×value[0]=60
4、确定遍历顺序
5、确定返回值
从 [0,N) 中取任意物品,放进容量为 W 的背包后的最大价值,即为 dp[N−1][W]。
6、代码示例
function maxValue(weights: number[], values: number[], bagWeight: number) {
const length = weights.length;
const dp = Array.from({ length }, () => new Array(bagWeight + 1).fill(0));
for (let j = 1; j <= bagWeight; j++) {
dp[0][j] = Math.floor(j / weights[0]) * values[0];
}
for (let i = 1; i < length; i++) {
for (let j = 1; j <= bagWeight; j++) {
dp[i][j] = dp[i - 1][j];
for (let k = 1; k * weights[i] <= j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * weights[i]] + k * values[i])
}
}
}
return dp[length - 1][bagWeight];
}
状态压缩
1、确定 dp 数组以及其含义
💥 dp[j] 代表容量为 j 的背包所能承载的最大价值,其中 j∈[0,W]。
2、确定 dp 对应的状态方程
dp[j]=max(dp[j],dp[j−weights[i]]+values[i])
其中 i∈[0,N)。💥NOTE:可以发现完全背包问题使用一维 dp 状态数组求解时的状态转移方程与 0−1 背包的状态转移方程完全一致。这是因为完全背包与 0−1 背包仅在物品的重复使用上是不同的,0−1 背包是靠 倒序遍历背包容量 来保证每一个物品最多被使用一次,如果正序遍历背包,那就变成了完全背包。(详见4、确定遍历顺序)
3、确定 dp 初始状态
容量为 0 的背包,最大承载价值也恒为 0,即 dp[0]=0
💥NOTE:dp 初始值之所以能取 0,是因为要求总价值最大,故初始化时应取一个“局部最小值”,防止后续计算被初始值所覆盖。又由于物品的价值均为正整数,故这个局部最小值取 0 即可。(其他位置的初始值也可设置为“局部最小值”)
jdp[j]0010203040
4、确定遍历顺序
组合问题,不强调放入顺序
5、确定返回值
容量为 W 的背包所能承载的最大价值为 dp[W],即为返回值
6、递推求解与代码
i=0 时,weights[i]=1,values[i]=15,dp[j]=max(dp[j],dp[j−1]+15)
j 从 weights[0] (值为 0) 开始遍历,计算出:
-
j=1 时,dp[1]=max(dp[1],dp[0]+15)=max(0,15)=15
-
j=2 时,dp[2]=max(dp[2],dp[1]+15)=max(0,15+15)=30
-
j=2 时,dp[3]=max(dp[3],dp[2]+15)=max(0,30+15)=45
-
j=3 时,dp[4]=max(dp[4],dp[3]+15)=max(0,45+15)=60
故,
jdp[j]00115230345460
i=1 时,weights[i]=3,values[i]=20,dp[j]=max(dp[j],dp[j−3]+20)
j 从 weights[1] (值为 3) 开始遍历,计算出:
- j=3 时,dp[3]=max(dp[3],dp[0]+20)=max(45,0+20)=45
- j=4 时,dp[4]=max(dp[4],dp[1]+20)=max(60,15+20)=60
故,
jdp[j]00115230345460
i=2 时,weights[i]=4,values[i]=30,dp[j]=max(dp[j],dp[j−4]+30)
j 从 weights[2] (值为 4) 开始遍历,计算出:
- j=4 时,dp[4]=max(dp[4],dp[0]+20)=max(60,20)=60
故,
jdp[j]00115230345460
示例代码
function maxValue(weights: number[], values: number[], bagWeight: number) {
const dp = Array.from({ length: bagWeight + 1 }, () => 0);
for(let i = 0, len = weights.length; i < len; i++) {
for(let j = weights[i]; j <= bagWeight; j++) {
dp[i] = Math.max(dp[i], dp[j - weights[i]] + values[i])
}
}
return dp[bagWeight];
}
题解
完全背包中的 组合 问题:
完全背包中的 排列 问题
- 🎡 推荐使用一维 dp,只能先遍历背包,再遍历物品。
分析与总结
Q4:完全背包压缩算法中,是先遍历物品再遍历背包,是否可以倒置遍历顺序,即先遍历背包再遍历物品?
先说结论:💥可以但要区分情况。可以的原因是因为对于 1 维 dp 情况,两层循环的顺序并不影响 dp[j] 的计算,是因为 dp[j] 是根据下标 j 之前对应的 dp[k](其中 k<j)计算得到的,只要保证 dp[k] 都是经过计算的即可。区分情况的原因是要看问题的本质是组合还是排列,组合问题 只能先遍历物品,再遍历背包;排列问题只能先遍历背包,再遍历物品。
总结
| 一维 dp | 二维 dp |
|---|
| 0-1 背包 | 必须先遍历物品,再 倒序 遍历背包,遍历顺序不可颠倒 (否则会重复加入物品) | 可以先遍历物品,再遍历背包,遍历顺序可颠倒,但前者方式易于理解 |
| 完全背包 | 组合问题:先遍历物品,再遍历背包; 排列问题:先遍历背包,再遍历物品 | 更适合 组合问题:先遍历物品,再遍历背包,亦可先遍历背包,再遍历物品 |
参考
一篇文章吃透背包问题
宫水三叶-那就从 0-1 背包问题开始讲起吧 ...