代码随想录算法训练营day37

代码随想录算法训练营

—day37


前言

今天是算法营的第37天,希望自己能够坚持下来!
今日任务:
● 完全背包
● 518. 零钱兑换 II
● 377. 组合总和 Ⅳ
● 70. 爬楼梯 (进阶)


一、完全背包

题目链接
文章讲解
视频讲解

完全背包:
跟01背包的区别就是完全背包的物品是可以无限次使用的。

二维dp数组

思路:

  1. dp[i][j]的定义为:跟01背包一样,表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。

  2. 递归公式:dp[i][j] = max(dp[i-1][j], dp[i][j-weight(i)] + vaule[i])
    ①在01背包问题中,物品只能放一次,所以如果选定物品i,那么组合的个数就取决于i以前的物品的组合数:dp[i-1]
    ②而完全背包问题里,物品可以重复放,对于一个物品i来说,即使是选定了一个物品i,因为仍然可能放入重复的物品i,所以取决于i和i以前的物品的组合数:dp[i]

  3. 初始化:由递推公式可知,需要初始化最左列和最上行。
    ①背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0,最左列初始化为0。
    ②最上行时放物品0,因为可以放无限次,那么就有当j >= weight[0]时,dp[0][j] 如果能放下weight[0]的话,就一直装。
    for (int j = weight[0]; j <= bagWeight; j++) //空间充足的话,一直累加value[0]
    dp[0][j] = dp[0][j - weight[0]] + value[0];

    在这里插入图片描述在这里插入图片描述

  4. 遍历顺序:对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。所以先遍历物品还是背包都可以。

  5. 举例推导dp数组:
    在这里插入图片描述

代码如下:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, bagWeight;
    int w, v;
    cin >> n >> bagWeight;
    vector<int> weight(n);
    vector<int> value(n);
    for (int i = 0; i < n; i++) {
        cin >> weight[i] >> value[i];
    }

    vector<vector<int>> dp(n, vector<int>(bagWeight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagWeight; j++)
        dp[0][j] = dp[0][j - weight[0]] + value[0];

    for (int i = 1; i < n; i++) { // 遍历物品
        for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
        }
    }

    cout << dp[n - 1][bagWeight] << endl;

    return 0;
}

一维dp数组

  1. dp[j]的定义为:跟01背包一样,背包容量为j,能装最大价值dp[j]的物品
  2. 递归公式:完全背包和01背包的递归公式区别在于dp[i],但因为一维dp数组没有i维度,所以完全背包和01背包的一维dp数组是一样的,dp[j] = dp[j - weight[i]] + vaule[i]
  3. 初始化:初始化成0就可以了。
  4. 遍历顺序:在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的。因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

代码如下:

#include<iostream>
#include<vector>
using namespace std;

int main() {
    int n,v;
    
    cin >> n >> v;
    
    vector<int> weight(n);
    vector<int> value(n);
    for (int i = 0; i < n; i++) {
        cin >> weight[i] >> value[i];
    }
    vector<int>dp (v + 1, 0);
    
    //这里先遍历物品或者背包都可以
    for (int i = 0; i < n; i++) { //遍历物品
        for (int j = weight[i]; j <= v; j++) { //遍历背包,因为物品可以重复选择,所以从头开始遍历
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    
    cout << dp[v] << endl;
    
    return 0;
}

二、518.零钱兑换II

题目链接
文章讲解
视频讲解

二维dp数组

思路:
本题求的是装满这个背包的物品组合数是多少。因为每一种面额的硬币有无限个,所以这是完全背包。但又不是纯完全背包。纯完全背包是求最大价值是多少,这道题是求组合数是多少。

  1. dp[i][j]的定义为:用硬币i凑出金额j的组合有dp[i][j]种
  2. 递归公式:dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]] 不放i+放i (预留coins[i]的位置,但因为可以重复放,仍有dp[i]种组合) ,如果j-coins[i] 小于0,就是放不下,那就等于不放i,dp[i-1][j]
  3. 初始化:dp[0][j]:当j能整除硬币i时,说明可以凑出j,有1种组合,否则就是0。
    dp[i][0], 用物品i(即coins[i]) 装满容量为0的背包,都有一种方法,即不装。
    所以 dp[i][0] 都初始化为1

需要初始化最左列和最上行:在这里插入图片描述

  1. 遍历顺序:二维DP数组的完全背包的两个for循环先后顺序是无所谓的。先遍历背包还是先遍历物品都可以。
  2. 打印DP数组:
    以amount为5,coins为:[2,3,5] 为例:
    dp数组应该是这样的:
    1 0 1 0 1 0
    1 0 1 1 1 1
    1 0 1 1 1 2

代码如下:

class Solution {
public:
    //二维dp
    //dp[i][j]:用硬币i凑出金额j的组合有dp[i][j]种
    //dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]] 不放i+放i(预留coins[i]的位置,但因为可以重复放,仍有dp[i]种组合) 
    //如果j-coins[i] 小于0,就是放不下,那就等于不放i,dp[i-1][j]
    //初始化 dp[0][j]:当j能整除硬币i时,说明可以凑出j,有1种组合,否则就是0
    //dp[i][0]:要凑出0,就是全都不放,所以是1种组合
    //遍历顺序 先遍历物品再比遍历背包
    int change(int amount, vector<int>& coins) {
        //这里用int的话leetcode数据会溢出,需要使用uint64_t 64位无符号整数类型
        //补充:unsigned long long在大多数平台上也表示64位无符号整数,但会根据平台的不同而有差异
        //而uint64_t是所有平台通用的64位无符号整数类型
        vector<vector<uint64_t>> dp (coins.size(), vector<uint64_t>(amount+1, 0));

        //初始化最左列
        for (int i = 0; i < coins.size(); i++) {
            dp[i][0] = 1;
        }

        //初始化最上行
        for (int j = 0; j <= amount; j++) {
            if (j % coins[0] == 0) dp[0][j] = 1;
        }

        //以下遍历顺序行列可以颠倒
        for (int i = 1; i < coins.size(); i++) { //行,遍历物品,从物品1开始,物品0已经初始化了
            for (int j = 0; j <= amount; j++) { //列,遍历背包
                if (j < coins[i]) dp[i][j] = dp[i-1][j];
                else dp[i][j] = dp[i-1][j] + dp[i][j - coins[i]];
            }
        }

        return dp[coins.size()-1][amount];
    }
};

一维dp数组

  1. dp[j]:凑成总金额j的货币组合数为dp[j]
  2. 递归公式:dp[j] = dp[j] +dp[j-coins[i]] 不放i+放i (预留coins[i]的位置) ,因为是求组合数,所以两种情况的组合数相加。
  3. 初始化:dp[0]: 装满容量为0的背包,都有一种方法,即不装。dp[0] = 1;
  4. 遍历顺序:要求组合数,需要先遍历物品再比遍历背包(正序)。
    在纯完全背包中,是可以调换物品和背包的遍历顺序的,因为求的是最大金额,对凑成的元素顺序没有要求。

如果先遍历背包,再遍历物品,是先固定的背包容量,然后物品一个个放进去,这样的话就会出现{a,b}{b,a}两种集合,如下图所示:
遍历背包j=1时,记录了dp[1] = 1 ->集合{1}
遍历背包j=2时,记录了dp[2] = 2 ->集合{2}和{1,1}两种方法
遍历背包j=3时,遍历硬币1,先选定了硬币1,容量还剩2,所以是dp[2]种方法,也就是将1加入到dp[2]的两个集合中,得到集合{1,2}和{1,1,1}
遍历硬币2,先选定了硬币2,容量还剩1,所以是dp[1]种方法,将2加入到dp[1]的集合中,得到集合{2,1}
此时就得到了{1,2}和{2,1},不符合求组合的要求
在这里插入图片描述代码如下:

class Solution {
public:
    //一维dp:
    //dp[j]含义:凑满金额为j的硬币组合数位dp[j]
    //递推公式 dp[j] = dp[j] + dp[j-coins[i]] 就是二维dp把dp[i]维度去掉
    //初始化:dp[0] = 1 凑满金额0的方法是1,全都不放
    //遍历顺序,只能先遍历物品再遍历背包,求的是组合,如果反过来的话会出现[物品1,物品2],[物品2,物品1]这种组合
    int change(int amount, vector<int>& coins) {
        vector<uint64_t> dp(amount + 1, 0);
        dp[0] = 1;

        for (int i = 0; i < coins.size(); i++) { //遍历物品,从物品0开始
            for (int j = coins[i]; j <= amount; j++) { //遍历背包,只需要从能放下物品i的背包开始
                dp[j] += dp[j - coins[i]]; 
            }
        }

        return dp[amount];
    }
};

三、377. 组合总和 Ⅳ

题目链接
文章讲解
视频讲解

思路:
本题就是完全背包求排列。思路是跟上一题的518.零钱兑换II一样,只是遍历顺序需要改变。

  1. dp[i]: 凑成目标正整数为i的排列个数为dp[j]
  2. 递归公式:dp[i] = dp[i] + dp[i - nums[j]] (放数字i和不放数字i的组合数相加)
  3. 初始化:因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
  4. 遍历顺序:因为求排列数,所以是外层for遍历背包,内层for循环遍历物品。
  5. 举例推导dp数组:

在这里插入图片描述

代码如下:

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<uint64_t> dp(target+1, 0);
        dp[0] = 1;

        //要求的排列,需要先遍历背包
        for (int j = 1; j <= target; j++) { //遍历背包
            for (int i = 0; i < nums.size(); i++) { //遍历物品
                if (j < nums[i]) continue;
                dp[j] += dp[j - nums[i]]; 
            }
        }

        return dp[target];
    }
};

70. 爬楼梯(进阶版)

题目链接
文章讲解
视频讲解

思路:
可以抽象成完全背包,台阶是背包,每一次爬梯就是选择不同的物品,爬的阶数是物品的价值,也是重量。

  1. dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
  2. 递归公式:dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j],
    所以dp[i] += dp[i - j]
  3. 初始化:递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。
  4. 遍历顺序:这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样。所以先遍历背包,再遍历物品

代码如下:

#include<iostream>
#include<vector>
using namespace std;

int main() {
    //dp[j]:j个台阶,有dp[j]种方法可以爬到楼顶
    //台阶是背包,每一次爬梯就是选择不同的物品,爬的阶数是物品的价值
    int m, n; //m:最多可以走m步,有n个台阶
    cin >> n >> m;
    
    vector<int> dp(n+1, 0);
    dp[0] = 1;
    
    //遍历顺序,因为走台阶是有先后顺序的,需要求排列
    for (int j = 1; j <= n; j++) { //遍历背包
        for (int i = 1; i <= m; i++) { //遍历物品
            if (j >= i)
            	dp[j] += dp[j-i];
        }
    }
    
    cout << dp[n] << endl;
    return 0;
}

总结

01背包求最大价值:

  • 二维数组:递推公式dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),
    先遍历物品还是背包都可以。
    一维数组:递推公式dp[j] = max(dp[j], dp[j-weight[i]] + value[i]),
    必须先遍历物品,然后背包后序遍历。

纯完全背包,求装满背包的价值:

  • 二维数组:递推公式dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i]),
    先遍历物品还是背包都可以。
  • 一维数组:递推公式dp[j] = max(dp[j], dp[j-weight[i]] + value[i]),
    先遍历物品还是背包都可以。

完全背包,求组合数:

  • 1.二维数组:递推公式dp[i][j] = dp[i-1][j] + dp[i][j - weight[i]],
    先遍历物品还是背包都可以。
  • 一维数组:递推公式dp[j] += dp[j - weight[i]],
    必须先遍历物品,背包正序遍历。(如果求的是排列数的话,需要先遍历背包)

明天继续加油!

代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第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、付费专栏及课程。

余额充值