动态规划:完全背包理论基础
完全背包就是每个物品可以使用无数次,最后背包可以放的物品最大价值是多少。
01背包中:
for(i=0; i<weight.size; i++){
for(j=0; j>=weight[i]; j--){
dp[j] = max(dp[j], dp[j-weight[i]+value[i]);
}
}
而完全背包,里面的for循环会变成:
正序遍历,让物品的选用多次。
for(j=weight[i]; j<=bagsize;j++)//物品数量不受限制
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
先遍历物品再遍历背包。和先遍历背包再遍历物品的结果是一样的。
先遍历物品后遍历背包:
先遍历背包后遍历物品:
void test_CompletePack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_CompletePack();
}
518.零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个
dp[j]:凑成总金额j的货币组合数为dp[j]
dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加。
注意这里,如果先遍历物品再遍历背包,是组合;但是如果先遍历背包再遍历物品,是排列,因为{1,5}和{5,1}算不同的方法了。
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]]; //因为coins的重量均为1,所以直接加上
}
}
return dp[amount];
}
};
- 时间复杂度: O(mn),其中 m 是amount,n 是 coins 的长度
- 空间复杂度: O(m)
377. 组合总和 Ⅳ
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,其实就是求排列!
dp[i]: 凑成目标正整数为i的排列个数为dp[i]
求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
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] && dp[i] < INT_MAX - dp[i-nums[j]]){
dp[i] += dp[i-nums[j]];
}
}
}
return dp[target];
}
};
- 时间复杂度: O(target * n),其中 n 为 nums 的长度
- 空间复杂度: O(target)
C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。
70. 爬楼梯(进阶版)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
求装满背包有几种方法,递推公式一般都是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]
既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。
这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!
所以需将target放在外循环,将nums放在内循环。
每一步可以走多次,这是完全背包,内循环需要从前向后遍历。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n, m;
cin>>n>>m;
vector<int> dp(n+5, 0);
dp[0] = 1;
for(int i=1; i<=n; i++){ //从1到n可以取n
for(int j=1; j<=m; j++){ //从1到m可以取m
if(i-j>=0) dp[i] += dp[i-j]; //i可以等于j
}
}
cout<<dp[n]<<endl;
return 0;
}
- 时间复杂度: O(n * m)
- 空间复杂度: O(n)