代码随想录算法训练营第37天 | 52. 携带研究材料、LeetCode518.零钱兑换II、LeetCode377.组合总和、57. 爬楼梯

目录

52. 携带研究材料

题目描述

输入描述

输出描述

输入示例

输出示例

提示信息

LeetCode518.零钱兑换II

LeetCode377.组合总和

57. 爬楼梯 

题目描述

输入描述

输出描述

输入示例

输出示例

提示信息


52. 携带研究材料

题目描述

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的重量,并且具有不同的价值。

小明的行李箱所能承担的总重量为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。

输入描述

第一行包含两个整数,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.

思路:物品可以多次选择,与01背包最多只能选择一次物品不同,这就是完全背包问题。

其实大体思路和01背包其实差不多,纯粹的完全背包问题维护二维空间时,不管是先遍历物品还是背包都可以,与01背包一样。

需要注意的是完全背包的递归公式:dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+values[i])

01背包的递推公式:dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+values[i])

从上面加粗的地方就能看出不同来,之所以有这样的不同,就在于完全背包可以多次使用同一个物品,所以它是可以累加的。

同时,因为完全背包的规则,所以会使初始化的时候的递推公式也会变化,毕竟物品现在可以使用不止一次,意味着只要空间容量允许,就可以加入背包中。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N, V;
    cin >> N >> V;
    vector<int> weight(N);
    vector<int> values(N);
    for(int i = 0; i < N; i ++){
        cin >> weight[i];
        cin >> values[i];
    }
    
    vector<vector<int>> dp(N, vector<int>(V + 1, 0));//dp[i][j]表示0到i个物品在容量为j时所能装的最大价值
    for(int j = 0; j < V + 1; j ++){
        if(j >= weight[0]) dp[0][j] = max(dp[0][j], dp[0][j - weight[0]] + values[0]);
    }//注意这里的初始化相当于能够累加前面的元素值,这也是完全背包递推公式在每一行最直观的展示
    for(int i = 1; i < N; i ++){
        for(int j = 0; j < V + 1; 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]] + values[i]);
            //注意上面的dp[i][j]的面是dp[i][j-weight[i]]+values,
            //如果是01背包,就是dp[i-1][j-weight[i]]+values
        }
    }
    cout << dp[N - 1][V] << endl;
}

 当然,使用一维滚动数组同样可以完成完全背包问题,这里需要注意两点

第一点,两层循环不管是先遍历物品还是背包,都是可以的(当然仅限于纯粹的完全背包问题,后面一些问题在遍历顺序上还是有说法的,并不是两种都适用,因为融合了实际应用),这跟01背包只能先遍历物品,然后才能遍历背包不同;

第二点,完全背包遍历背包容量的那一层下标索引是逐渐增大的,也就是正序的,这与01背包容量这一层循环倒序遍历不同。究其原因就是因为物品可以多次使用,并且求dp[j]时,需要dp[j-weight[i]]前面的元素已经求出,所以需要使用正序。

下面是先遍历物品,再遍历背包容量的情况。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N, V;
    cin >> N >> V;
    vector<int> weight(N);
    vector<int> values(N);
    for(int i = 0; i < N; i ++){
        cin >> weight[i];
        cin >> values[i];
    }
    
    vector<int> dp(V + 1, 0);//这里的dp[j]是指背包容量为j时的所装的最大价值
    for(int i = 0; i < N; i ++){//先遍历物品,再遍历容量
        for(int j = weight[i]; j < V + 1; j ++){//注意这里的顺序
            dp[j] = max(dp[j], dp[j - weight[i]] + values[i]);
        }
    }
    cout << dp[V] << endl;
}

下面是先遍历背包容量,再遍历物品的情况。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N, V;
    cin >> N >> V;
    vector<int> weight(N);
    vector<int> values(N);
    for(int i = 0; i < N; i ++){
        cin >> weight[i];
        cin >> values[i];
    }
    
    vector<int> dp(V + 1, 0);//这里的dp[j]是指背包容量为j时的所装的最大价值
    for(int j = 0; j < V + 1; j ++){//先遍历容量,再遍历物品
        for(int i = 0; i < N; i ++){
            if(j >= weight[i]) dp[j] = max(dp[j], dp[j - weight[i]] + values[i]);
        }
    }
    cout << dp[V] << endl;
}

LeetCode518.零钱兑换II

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

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

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

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

思路:每一种面额的硬币有无限个,于是本题其实是一个完全背包的问题。

给定一个背包容量为amount,将其装满,求有多少种方法。

之前做目标和的题目的时候讲过类似的,像这样的问题递推公式一般都是dp[j]+=dp[j-nums[i]],注意这里是将nums[i]的大小当成了第i个物品所占空间容量大下,以及所拥有的价值。

所以这道题就简单起来了。

这里先讲一下使用二维数组维护dp数组的情况,首先需要注意的是初始化的时候,不再像01背包的时候只将j==nums[0]的元素置为1,而是采用递推公式赋值,因为元素可以使用多次!

然后这里再注意一下两层循环中的递推公式,上面那道题已经强调过了。注意这些基本就没什么为题了。

    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));//dp[i][j]表示第0到i个物品中选,将容量为j的背包装满的组合数
        for(int i = 0; i < coins.size(); i ++){
            dp[i][0] = 1;//初始化最左边一列值
        }
        for(int j = coins[0]; j < amount + 1; j ++){
            dp[0][j] += dp[0][j - coins[0]];//初始化第一行的值
        }
        for(int i = 1; i < coins.size(); i ++){
            for(int j = 0; j < amount + 1; 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];
    }

 当然还是可以使用一维数组来维护。

但是请注意,这里的遍历顺序有讲究了。

先遍历物品,再遍历容量,求的是组合数;

先遍历容量,再遍历物品,求的是组合数。

为什么呢?

这样像,当先遍历物品,比如有个数组为[1,5],amount为3,那么先遍历物品,就只会有{1,5}这个解,因为第二个物品(值为5)不可能出现在第一个物品(值为1)之前,所以最后求的就是组合数;而如果先遍历背包,后遍历物品,第二个物品(值为5)有可能出现在第一个物品(值为1)之前,所以会有{1,5},{5,1}这样的解,这就是排列。

如果还没太明白可以自己手动模拟,写出来基本上就明白了。

于是乎,这道题求的是组合数,所以必须先遍历物品,再遍历背包容量。

    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);//dp[j]表示总金额为j时,凑满j的组合个数
        dp[0] = 1;//当总和为0时,可以不放任何物品,也相当于一种方法
        for(int i = 0; i < coins.size(); i ++){//先遍历物品
            for(int j = coins[i]; j < amount + 1; j ++){//再遍历背包,这样就是的组合数,反之求的是排列数
                   dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }

时间复杂度:O(mn)

空间复杂度:O(m)

LeetCode377.组合总和

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

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

思路:经过上面的题后,再看这道题,就开始简单起来了。

本题给定了背包容量target,求将其装满有多少种方法,并且从题目给出的测试用例可以看出,本题不再是组合,而是排列。

所以先遍历背包容量,再遍历物品即可。

这里还需要注意一个点,因为力扣给出的测试用例中,存在有两个数相加超过了int的限,于是需要有这样一个判断,dp[j] < INT_MAX - dp[j - nums[i]],这样能够确保能够继续更新dp数组,最后返回结果。

    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target + 1, 0);//dp[j]表示装满背包容量的组合个数
        dp[0] = 1;//初始化
        for(int j = 0; j < target + 1; j ++){//先遍历背包容量
            for(int i = 0; i < nums.size(); i ++){//再遍历物品
                if(j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]]) dp[j] += dp[j - nums[i]];
                //这里出现了两个数之和超限了,所以需要加上判断dp[j] < INT_MAX - dp[j - nums[i]]
            }
        }
        return dp[target];
    }

时间复杂度:O(mn)

空间复杂度:O(m)

57. 爬楼梯 

题目描述

假设你正在爬楼梯。需要 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或者2,但是这里是能迈1到m步!

所以这道题可以使用动态规划来解,将题目转化成给定背包容量(楼梯阶数n),尝试用1到m步来将其填满,所有的方法数有多少。注意这里的1到m可以使用多次,也就是说这一次迈一步,下一次还能迈一步,所以这又是一个完全背包问题。

同时我们还可以知道,首先迈一步,再迈两步,与首先迈两步,再迈一步是不同的结果,所以这又是求排列的问题。

这样的题目我们刚才才做过!

所以注意一下遍历顺序,先容量后物品,再注意一下初始值,也就没什么问题了。

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n, m;
    cin >> n >> m;
    vector<int> dp(n + 1, 0);//dp[j]表示到达j层楼梯时的方法总数
    dp[0] = 1;//初始化
    for(int j = 1; j < n + 1; j ++){
        for(int i = 1; i <= m; i ++){
            if(j - i >= 0) dp[j] += dp[j - i];
        }
    }
    cout << dp[n] << endl;
    return 0;
}

时间复杂度:O(nm)

空间复杂度:O(n)

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值