DAY47:动态规划(九)完全背包理论基础


课程链接: 代码随想录 (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循环都能颠倒。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值