《代码随想录第四十二天》——完全背包、零钱兑换II、组合总和IV、爬楼梯(进阶)

《代码随想录第四十二天》——完全背包、零钱兑换II、组合总和IV、爬楼梯(进阶)

本篇文章的所有内容仅基于C++撰写。

1. 完全背包

1.1 题目

携带研究材料(可重复)
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。
小明的行李箱所能承担的总重量是有限的,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

  • 输入描述
    第一行包含两个整数,n,v,分别表示研究材料的种类和行李所能承担的总重量
    接下来包含 n 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
    输出描述

  • 输出一个整数,表示最大价值。

  • 输入示例
    4 5
    1 2
    2 4
    3 4
    4 5

  • 输出示例
    10

  • 提示信息:第一种材料选择五次,可以达到最大值。

  • 数据范围:
    1 <= n <= 10000;
    1 <= v <= 10000;
    1 <= wi, vi <= 10^9.

1.2 分析

完全背包和01背包的不同之处就在于完全背包中的物品数量无限,是可重复放入的。
假如背包最大重量为4,物品为:
重量 价值
物品0 1 15
物品1 3 20
物品2 4 30
每件商品都有无限个,问背包能背的物品最大价值是多少?
开始动规分析五部曲。

  1. dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少。
  2. 递推公式
    这里依然拿dp[1][4]的状态来举例: (01背包理论基础(二维数组)中也是这个例子,要注意下面的不同之处)
    求取 dp[1][4] 有两种情况:
  • 放物品1
  • 还是不放物品1
    如果不放物品1, 那么背包的价值应该是 dp[0][4] 即 容量为4的背包,只放物品0的情况。推导方向如图:
    在这里插入图片描述
    如果放物品1, 那么背包要先留出物品1的容量,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。
    容量为1,只考虑放物品0 和物品1 的最大价值是 dp[1][1], 注意 这里和 01背包理论基础(二维数组)有所不同,01背包中是dp[0][1]。这是因为01背包中物品都只有一个,没放肯定就不在背包中;而完全背包的物品有无数个,即使当前没放,在之前的背包容量中也可能放入了。如图:
    在这里插入图片描述
    两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值):dp[1][4] = max(dp[0][4], dp[1][1] + 物品1 的价值)
    得到递推公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
  1. dp数组如何初始化
    首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
    再看其他情况:
    状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。就是在背包容量允许的情况下,一直往里装物品0.
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagWeight; j++) {
    dp[0][j] = dp[0][j - weight[0]] + value[0]; 
}
  1. 确定遍历顺序
    01背包理论基础(二维数组)中,先遍历物品还是先遍历背包都是可以的。
    因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。
    完全背包也一样,既可以 先遍历物品再遍历背包,也可以 先遍历背包再遍历物品。

另外要注意,如果使用一维数组,完全背包的先遍历背包还是先遍历物品都可以,不像01背包中必须先遍历物品再遍历背包。但是,完全背包中的物品和背包(主要是背包)都必须从前向后遍历。

1.3 代码

  1. 二维数组
#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];
            //在完全背包中,不存在重量为0的物品,不然就会无限地放下去,所以这里的j完全可以从1开始取值
            else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
        }
    }

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

    return 0;
}
  1. 一维数组
#include <iostream>
#include <vector>
using namespace std;

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

    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;

    return 0;
}

2. 零钱兑换II

2.1 题目

零钱兑换
给你一个整数数组 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.2 分析

本题和纯完全背包不一样,纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!注意题目描述中是凑成总金额的硬币组合数,组合不强调元素之间的顺序,排列强调元素之间的顺序。
动规五部曲:

  1. 确定dp数组的下标及含义
    定义二维dp数值 dp[i][j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。
  2. 确定递推公式
    在目标和中详解讲解了装满背包有几种方法,二维DP数组的递推公式: dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]],即不装当前物品的方法+腾出背包装当前物品的方法,这个跟爬楼梯有点像。
    所以本题递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]] ,区别依然是 dp[i - 1][j - nums[i]] 和 dp[i][j - nums[i]]。
  3. dp数组初始化
  • 最上行dp[0][j] 如何初始化呢?dp[0][j]的含义:用「物品0」(即coins[0]) 装满 背包容量为j的背包,有几种组合方法。
    如果 j 可以整除 物品0,那么装满背包就有1种组合方法。
for (int j = 0; j <= bagSize; j++) {
    if (j % coins[0] == 0) dp[0][j] = 1;
}
  • 最左列如何初始化呢?dp[i][0] 的含义:用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。
    都有一种方法,即不装。所以 dp[i][0] 都初始化为1

#4. 确定遍历顺序
二维数组的遍历,先物品还是先背包都可以。

2.3 代码

  1. 二维数组
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int bagSize = amount;

        vector<vector<uint64_t>> dp(coins.size(), vector<uint64_t>(bagSize + 1, 0));

        // 初始化最上行
        for (int j = 0; j <= bagSize; j++) {
            if (j % coins[0] == 0) dp[0][j] = 1;
        }
        // 初始化最左列
        for (int i = 0; i < coins.size(); i++) {
            dp[i][0] = 1;
        }
        // 以下遍历顺序行列可以颠倒
        for (int i = 1; i < coins.size(); i++) { // 行,遍历物品
            for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
                if (coins[i] > j) 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][bagSize];
    }
};
  1. 一维数组
    注意:本题求的是组合,必须先遍历物品再遍历背包,否则会出现重复的方法(即排列)。
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<uint64_t> dp(amount + 1, 0); // 防止相加数据超int
        dp[0] = 1; // 只有一种方式达到0
        for (int i = 0; i < coins.size(); i++) { // 遍历物品
            for (int j = coins[i]; j <= amount; j++) { // 遍历背包
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount]; // 返回组合数
    }
};
  • 时间复杂度: O(mn),其中 m 是amount,n 是 coins 的长度
  • 空间复杂度: O(m)

3. 组合总和IV

3.1 题目

组合总和IV
给你一个由 不同 整数组成的数组 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

3.2 分析

题上说的是组合,实际上求的是排列,那我们书接上回,用先背包后物品的一维数组来解决。

动规五部曲分析如下:

  1. 确定dp数组以及下标的含义
    dp[i]: 凑成目标正整数为i的排列个数为dp[i]

  2. 确定递推公式
    递推公式是dp[i] += dp[i - nums[j]];即不放入当前物品的排列方法和腾出背包放入当前物品的排列方法。

  3. dp数组如何初始化
    因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,因为dp[0]反正是没有意义的,就拿它的值来给后面的dp[i]做初始的启动值,不然数组的所有值会一直为0。
    至于非0下标的dp[i]应该初始为多少呢?初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。

  4. 确定遍历顺序
    个数可以不限使用,说明这是一个完全背包。
    本题要求的是排列,那么这个for循环嵌套的顺序可以有说法了。

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。

所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。

3.3 代码

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target + 1, 0);
        dp[0] = 1;
        for (int i = 0; i <= target; i++) { // 遍历背包
            for (int j = 0; j < nums.size(); j++) { // 遍历物品
                if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {//能装下当前物品且价值不会溢出
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
};
  • 时间复杂度: O(target * n),其中 n 为 nums 的长度
  • 空间复杂度: O(target)

4. 爬楼梯(进阶版)

4.1 题目

爬楼梯(进阶版)
题目描述
假设你正在爬楼梯。需要 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 阶
    2 阶 + 1 阶

4.2 分析

本题的区别在于,不再是只能爬一阶或两阶楼梯,而是可以一直爬到最顶端(m阶),求爬楼梯的方法数。本题跟上一题的原理类似,求的也是要排列的完全背包,只是递推公式有变化。

动规五部曲分析如下:

  1. 确定dp数组以及下标的含义
    dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。

  2. 确定递推公式
    递推公式一般都是dp[i] += dp[i - nums[j]];本题呢,dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]
    那么递推公式为:dp[i] += dp[i - j]

  3. dp数组如何初始化
    既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。
    下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果。

  4. 确定遍历顺序
    这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!
    所以需将target放在外循环,将nums放在内循环。
    每一步可以走多次,这是完全背包,内循环需要从前向后遍历。

4.3 代码

#include <iostream>
#include <vector>
using namespace std;
int main() {
    int n, m;
    while (cin >> n >> m) {
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= n; i++) { // 遍历背包
            for (int j = 1; j <= m; j++) { // 遍历物品
                if (i - j >= 0) dp[i] += dp[i - j];
            }
        }
        cout << dp[n] << endl;
    }
}
  • 时间复杂度: O(n * m)
  • 空间复杂度: O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值