文章目录
- 完全背包示例
- 与01背包的区别:遍历顺序
- 常规遍历写法
- DP状态图-为什么背包正序就能放进来重复物品
- for循环的嵌套,外层物品内层背包能否颠倒?
- for嵌套顺序颠倒的遍历写法
- 测试示例
- 面试题目
- 总结
课程链接: 代码随想录 (programmercarl.com)
完全背包示例
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,我们通过纯完全背包问题理解原理。
题目示例:
背包最大重量为4。
物品为:
每件商品都有无限个!
问背包能背的物品最大价值是多少?
与01背包的区别:遍历顺序
完全背包在写法上,与01背包唯一区别就是遍历顺序。
常规遍历写法
01背包遍历顺序:
for(int i=0;i<weight.size();i++){//物品在外
for(int j=bagWeight;j>=weight[i];j--){//背包在内,且背包倒序
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
遍历顺序和倒序相关问题:DAY45:动态规划(六)背包问题优化:一维DP解决01背包问题_大磕学家ZYX的博客-CSDN博客
01背包内嵌的背包容量循环是倒序遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
for(int i=0;i<weight.size();i++){//仍然是物品在外
for(int j=weight[i];j<=bagWeight;j++){//背包容量是正序遍历
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
DP状态图-为什么背包正序就能放进来重复物品
可以通过DP状态图看一下原因。如下图,可以解释为什么背包容量正序遍历,就会放进来很多重复的物品。图中可知,如果正序,同一物品的情况下,就会累加dp[j[-weight[i]]]
,导致这个物品i被放进去很多次。
for循环的嵌套,外层物品内层背包能否颠倒?
其实还有一个很重要的问题,为什么遍历物品在外层循环,遍历背包容量在内层循环?
之前的博客整理过,01背包遍历物品必须在外层循环,原因是一维dp的写法,背包容量是倒序遍历(为了不重复放入[j-weight[i]]
的物品),而如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
01背包物品在内的情况:(01背包容量倒序)
DAY45:动态规划(六)背包问题优化:一维DP解决01背包问题_大磕学家ZYX的博客-CSDN博客
而在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
原因完全背包,背包容量的for循环是正序遍历,是dp[j]
是根据 下标j之前所对应的dp[j]
计算出来的。 只要保证下标j之前的dp[j]
都是经过计算的就可以了。
遍历物品在外层循环,遍历背包容量在内层循环,状态如图:
蓝色字体是dp[3]
和dp[4]
的推导过程。
遍历背包容量在外层循环,遍历物品在内层循环,状态如图:
蓝色字体是dp[3]
的推导过程,绿色字体是dp[4]
的推导过程。
根据上图结果可以看出,因为完全背包是背包正序遍历,因此即使背包循环在外面,dp[3]
和dp[4]
的推导也不受影响。相当于避免了01背包里面,颠倒遍历顺序会只放进去一个物品的问题。
完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。
01背包里,因为dp[j]
计算需要dp[j-weight[i]]
,而如果j在外层,遍历到dp[j]
的时候,dp[j-weight[i]]
根本没有数据。因此会出现问题。但完全背包正序遍历,并不影响dp[j-weight[i]]
的数值。因此可以颠倒。
for嵌套顺序颠倒的遍历写法
背包在外层,物品在内层的遍历写法:
- 注意颠倒嵌套顺序的写法,需要注意下标越界问题!
for(int j=0;j<=bagWeight;j++){
for(int i=0;i<weight.size();i++){
if(j-weight[i]) //注意颠倒嵌套顺序的写法,需要注意下标越界问题!
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
}
}
测试示例
常规遍历顺序:
// 先遍历物品,在遍历背包
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_CompletePack();
}
for颠倒的遍历顺序:
// 先遍历背包,再遍历物品
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_CompletePack();
}
面试题目
可能出的面试题目为,纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后再问,两个for循环的先后是否可以颠倒?为什么?
纯完全背包问题里,for循环可以颠倒,但是01背包不行。
原因是,01背包里,因为dp[j]
计算需要dp[j-weight[i]]
,而如果j在外层,遍历到dp[j]
的时候,dp[j-weight[i]]
根本没有数据。因此会出现问题。
但纯完全背包问题,是正序遍历,遍历到dp[j]的时候,dp[j-weight[i]]
的数值已经计算完成。因此可以颠倒。
总结
注意,全文说的都是对于纯完全背包问题,其for循环的先后循环是可以颠倒的!
但如果题目稍稍有点变化,就会体现在遍历顺序上。
如果问装满背包有几种方式的话, 那么两个for循环的先后顺序就有很大区别了。
如果我们先遍历背包后遍历物品,得到的就是方案的排列数;先遍历物品再遍历背包,得到的就是方案组合数。这个结论可以在后面的例题:518.零钱兑换Ⅱ 377.组合总和Ⅳ中进行进一步的理解。
原理参考:【总结】用树形图和剪枝操作理解完全背包问题中组合数和排列数问题_先遍历物品后遍历背包是组合数_Calculus2022的博客-CSDN博客
因此并不是所有的完全背包题目,for循环都能颠倒。