代码随想录算法训练营第四十四天| 完全背包、518. 零钱兑换 II、377. 组合总和 Ⅳ

一、完全背包

题目链接/文章讲解/视频讲解:https://programmercarl.com/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85.html

状态:已解决

1.问题介绍

        完全背包的模板题目:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

        它与01背包的唯一区别在于物品的数量(01背包一种物品只有一个,而完全背包有无限个)。

2.解法

        因为完全背包只有物品数量不一样,其余都是与01背包一致的,故dp数组、递推公式都是不变的,要变的只有遍历顺序。在01背包中,我们内循环是从大到小去遍历的,母的是保证每个物品仅被添加一次。而完全背包物品是可以被添加多次的,也就是说,要从小到大去遍历。具体原因在动态规划:关于01背包问题,你该了解这些!(滚动数组)中也说过了,从小到大遍历代表第 i 个物品可以一直被添加到dp数组中。如图:

        这里还有一个问题: 为什么遍历物品在外层循环,遍历背包容量在内层循环?其实两种顺序都可以,因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。在一维背包-CSDN博客中,我说过了,因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那就是先计算最后一列的数值,而我们知道某个格子的数值是依赖左上角的值的,但此时左上角(倒数第二列)还没计算,故推不出正确结果,因此只能是物品先遍历。也就是倒序遍历决定了我们只能先遍历物品。而此题并不需要倒序遍历,因此内外层哪种遍历顺序都能使得左上角和正上方的格子先被计算了。

        遍历物品在外层循环,遍历背包容量在内层循环,状态如图:

         遍历背包容量在外层循环,遍历物品在内层循环,状态如图:

3.代码实现 

#include<bits/stdc++.h>
using namespace std;
int main(void){
    int N,V;
    cin>>N>>V;
    vector<int> w(N);
    vector<int> v(N);
    for(int i=0;i<N;i++){
        cin>>w[i]>>v[i];
    }

    vector<int> dp(V+1,0);
    for(int i=0;i<N;i++){
        for(int j=w[i];j<=V;j++){
            dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    cout<<dp[V];
}

二、518. 零钱兑换 II

题目链接/文章讲解/视频讲解:https://programmercarl.com/0518.%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2II.html

状态:已解决

1.思路 

        这道题就是典型的背包问题,且题目说了钱币的数量不限,故是背包问题中的完全背包。但此题跟纯完全背包又不一样,纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!求的是组合数!

        我们用动规五部曲来分析此题:

(1)确定dp数组以及下标含义:

        没有多余的维度,一维数组dp就够了。dp[j]:凑成总金额为 j 的货币组合数为dp[j]。

(2)确定递推公式:

        dp[j]就是所有dp[j - coins[i]]的值相加。

        所以递推公式为:dp[j] += dp[j - coins[i]]。

(3)dp数组初始化:

        首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。其含义是如果正好选了coins[i]后,也就是j-coins[i] == 0的情况表示这个硬币刚好能选,此时dp[0]为1表示只选coins[i]存在这样的一种选法。

(4)确定遍历顺序:

        本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢?

我在上面的完全背包理论中讲了完全背包的两个for循环的先后顺序都是可以的。但本题就不行了!

        因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!也就是说,在纯完全背包问题中,我们只关心最后的背包的总价值,不关心物品放进去的顺序

        而本题要求凑成总和的组合数,元素之间明确要求没有顺序。那么本题,两个for循环的先后顺序可就有说法了。

(1)我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。

for (int i = 0; i < coins.size(); i++) { // 遍历物品
    for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
        dp[j] += dp[j - coins[i]];
    }
}

        假设:coins[0] = 1,coins[1] = 5。那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。所以这种遍历顺序中dp[j]里计算的是组合数!

(2)外层for循环遍历背包(金钱总额),内层for遍历物品(钱币)的情况。

for (int j = 0; j <= amount; j++) { // 遍历背包容量
    for (int i = 0; i < coins.size(); i++) { // 遍历物品
        if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
    }
}

        背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5}(j = 6, coins[i] = 1) 和 {5, 1}(j = 6, coins[i] = 5)两种情况。

此时dp[j]里算出来的就是排列数!

        排列数多于组合数,故不可以这样遍历。

       

 那为什么在纯完全背包问题中就行呢?因为对于{1,5}和{5,1},它们放进去加起来背包得到的价值都是6,故效果是相等的。

(5) 举例推导dp数组:

2.代码实现 

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1,0);
        dp[0] = 1;
        for(int i=0;i<coins.size();i++){
            for(int j=coins[i];j<=amount;j++){
                dp[j] += dp[j-coins[i]];
            }
        }
        //for(int i=0;i<=amount;i++) cout<<dp[i]<<" ";
        return dp[amount];
    }
};

时间复杂度:O(mn)

空间复杂度:O(m) 

三、377. 组合总和 Ⅳ

题目链接/文章讲解/视频讲解:https://programmercarl.com/0377.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C%E2%85%A3.html

状态:已解决

1.思路 

        这题明面上是求组合,实际就是求排列。回溯是求排列问题的好手,但此题不需要打印集合,只需要求个数,因此还用不着回溯,动规足矣!

        这道题也是简单的完全背包问题,跟上一道题的区别无非就是一个求组合一个求排列,而我们在上道题说了组合和排列的区别实际就是遍历顺序的不同。组合的遍历顺序是先物品再容量,排序的遍历顺序是先容量再物品。因此,我们只需要在上一道题的基础上修改一下遍历顺序即可。

2.代码实现

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target+1,0);
        dp[0] = 1;
        for(int j=0;j<=target;j++){
            for(int i=0;i<nums.size();i++){
                if(j >= nums[i])
                    dp[j] += dp[j-nums[i]];
            }
        }
        return dp[target];
    }
};

时间复杂度:O(mn)   m为target的值,n为nums的长度

空间复杂度:O(m) 

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值