【代码随想录训练营第42期 Day37打卡 - 动态规划Part5 - 完全背包问题

目录

一、做题心得

二、完全背包模板

模板题:AcWing 3. 完全背包问题

题目链接

模板1:最大价值问题

模板2:组合排列数问题

组合数

排列数 

三、题目与题解

题目一:518. 零钱兑换 II

题目链接

题解:动态规划--完全背包

题目二:377. 组合总和 Ⅳ

题目链接

题解:动态规划--完全背包

题目三:57. 爬楼梯(卡码网)

题目链接

题解:动态规划--完全背包

三、小结


一、做题心得

今天是代码随想录打卡的第37天,来到了动态规划章节的完全背包问题。完全背包问题相较于前边打卡的01背包问题,由一个物品只能使用一次,变成了一个物品可以使用多次。那么代码上自然也会存在相应改动。不过由于都属于背包问题的范畴,思路上讲是一致的。因此,有了前边打卡的基础,今天的内容就比较简单了。

话不多说,直接开始今天的内容。

二、完全背包模板

模板题:AcWing 3. 完全背包问题

题目链接

3. 完全背包问题 - AcWing题库

模板1:最大价值问题
#include<bits/stdc++.h>
using namespace std;

const int N = 1005;
int n, m;
int v[N];
int w[N];
int dp[N] = {0};        //也可以vector数组

signed main() {
    cin>>n>>m;
    for (int i = 0; i < n; i++) {
        cin>>v[i]>>w[i];
    }
    for (int i = 0; i < n; i++) {               //遍历物品
        for (int j = 0; j <= m; j++) {              //注意:正序遍历背包(从0到最大容量)--表示物品可以重复使用--注意与01背包区别
            if (j >= v[i]) {             //当前背包能装下该物品时(注意是大于等于)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
            }
        }
    }
    cout<<dp[m];
    return 0;
}

其实完全背包和01背包问题的主要区别就在于:在遍历背包上,01背包采用逆序遍历,而完全背包采用顺序(正序)遍历--这是因为逆序遍历确定了每个物品只能选一次,而顺序遍历时每个物品可以选择多次。还有一个区别就是01背包只能先遍历物品再遍历背包,而完全背包两种顺序都可以。

还有一道模板题和这个一致:52. 携带研究材料(第七期模拟笔试) (kamacoder.com)

附一份代码:

#include<bits/stdc++.h>
using namespace std;

const int N = 10005;
int n, m;
int v[N];
int w[N];
int dp[N] = {0};

signed main() {
    cin>>n>>m;
    for (int i = 0; i < n; i++) {
        cin>>v[i]>>w[i];
    }
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= m; j++) {
            if (j >= v[i]) {
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
            }
        }
    }
    cout<<dp[m];
    return 0;
}

当然,这个模板是为了解决完全背包问题对于求得最大价值这一类问题,后边还会提到解决求得方案数(组合数或者排列数)这一类完全背包问题。

模板2:组合排列数问题

组合数:同一组合,不同顺序,一种情况

排列数:同一组合,不同顺序,不同情况

组合数

先遍历物品,再遍历背包

for (int i = 0; i < n; i++) {          //组合数--先遍历物品,再遍历背包
            for (int j = 0; j <= m; j++) {
                if (j >= v[i]) {            //注意:>=
                    dp[j] += dp[j - v[i]]; 
                }
            }
        }
排列数 

先遍历背包,再遍历物品

for (int i = 0; i <= m; i++) {          //排列数--先遍历背包,再遍历物品
            for (int j = 0; j < n; j++) {
                if (i >= v[j]) {            //注意:>=
                    dp[i] += dp[i - v[j]]; 
                }
            }
        }

三、题目与题解

题目一:518. 零钱兑换 II

题目链接

518. 零钱兑换 II - 力扣(LeetCode)

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

提示:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000
题解:动态规划--完全背包

这道题一看到每一种面额的硬币有无限个,就可以联想到这是一个完全背包的组合数问题

题目要求得出可以凑成总金额的硬币组合数,其实就是刚刚模板2中的组合数问题。(不讲求顺序)在这里物品就是硬币,dp[j]表示凑成金额总数为j的组合数。

这里需要注意的就是初始化 dp[0] = 1。

代码如下:

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        /*     完全背包问题--一维dp    */
        vector<int> dp(5001, 0);            //dp[j]:凑成金额总数为j的组合数
        dp[0] = 1;          //注意:初始化dp[0] = 1(后续中,若j == coins[i],选择当前硬币,就出现dp[0],若dp[0] == 0,组合数就没有增加)
        for (int i = 0; i < coins.size(); i++) {          //组合数--先遍历物品,再遍历背包
            for (int j = 0; j <= amount; j++) {
                if (j >= coins[i]) {            //注意:>=
                    dp[j] += dp[j - coins[i]]; 
                }
            }
        }
        return dp[amount];
    }
};

题目二:377. 组合总和 Ⅳ

题目链接

377. 组合总和 Ⅳ - 力扣(LeetCode)

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

示例 2:

输入:nums = [9], target = 3
输出:0

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 1000
  • nums 中的所有元素 互不相同
  • 1 <= target <= 1000
题解:动态规划--完全背包

这道题就是一个典型的完全背包的排列数问题,即每个组合的情况是需要讲求顺序的。

在这里需要注意的就是需要防溢出:为了防止dp[i] + dp[i - nums[j]] 过大溢出,我们需要保证 dp[i - nums[j]] < INT_MAX - dp[i]。

其余的思路上就跟上一题一致,只是排列数问题需要先遍历背包,再遍历物品。

代码如下:

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(1005, 0);
        dp[0] = 1;      //注意求方案数的背包问题:初始化dp[0] = 1
        for (int i = 0; i <= target; i++) {         //排列数--先遍历背包再遍历物品
            for (int j = 0; j < nums.size(); j++) {
                if (i >= nums[j] && dp[i - nums[j]] < INT_MAX - dp[i]) {        //题目数据保证答案符合 32 位整数范围:dp[i - nums[j]] < INT_MAX - dp[i]在这里用作防溢出
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
};

题目三:57. 爬楼梯(卡码网)

题目链接

57. 爬楼梯(第八期模拟笔试) (kamacoder.com)

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢? 

注意:给定 n 是一个正整数。

输入描述

输入共一行,包含两个正整数,分别表示n, m

输出描述

输出一个整数,表示爬到楼顶的方法数。

输入示例

3 2

输出示例

3

提示信息

数据范围:
1 <= m < n <= 32;

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶段
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶
题解:动态规划--完全背包

这题也比较简单,很容易想到完全背包的做法--完全背包的排列数问题(即需要讲求顺序)。

物品在这就是指你能爬台阶的步数(1--m相当于各个物品的体积),背包就是台阶。dp[i]表示台阶数为 i 时爬到楼顶的方案数(其实就是排列数)。

理清题意之后,这里也是直接套用模板2的排列数问题。

代码如下:

#include<bits/stdc++.h>
using namespace std;

int n, m;
vector<int> dp(35, 0);

signed main() {
    cin>>n>>m;
    dp[0] = 1;
    for(int i = 0; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (i >= j) {
                dp[i] += dp[i - j];
            }
        }
    }
    cout<<dp[n];
    return 0;
}

三、小结

有了前边01背包的打卡练习,今天完全背包的问题也变得容易理解。理解了模板的由来之后,今天的打卡基本上就是模板的套用。最后,今天的打卡到此结束,后边将会继续加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值