给定你一个固定的背包容量capacity
,和两个数组weigth,value
,分别表示下标为i
的物品的重量和价值,每件物品有无数件,问你背包能够存放下物品的最大价值是多少?
这与之前的01背包极其拓展问题问题有很打不通,01背包的物品是有限个,而完全背包问题的物品是无限个,因此,所推倒的状态转移方程
不同。
比如:
物品 重量 价值
1 1 15
2 2 10
3 3 20
如果背包的容量是4,此时如果用使用01背包的思想去解决这个问题,那么是得不到最优解60,即装4件物品1。
01背包的一维dp数组中,外层循环表示遍历物品,而内存循环是从后向前遍历背包容量。而完全背包则是在内层循环中,从weight[i]开始一直遍历到背包的容量,因为每一次遍历物品时,我都从背包的weight[i]容量开始向后遍历,则我一定可以得到第j
容量下的最大物品价值。
01背包的遍历过程如下:
for(int i = 0;i<weigth.size();i++)
{
for(int j = bag_weight;j>=weight[i];j--)
{
//从后向前遍历
dp[j] = max(dp[j,dp[j - weight[i]] + value[i]);
}
}
而完全背包的遍历过程则是如下:
for(int i = 0;i<weigth.size();i++)
{
//从当前能装下weight[i]的位置开始向后遍历
for(int j = weight[i];j<=bag_weight;j++)
{
dp[j] = max(dp[j],dp[j - weight[i]] + value[i]);
}
}
下面讲解由完全背包拓展出来的题目:
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
题目链接:leetcode518.零钱兑换II
示例 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中给出的答案中,全部都是组合数,与硬币放的位置无关,这也是这道题目的关键所在,如果是排列数则会有所不同,下面也会讲到这样的题目。
dp[j] += dp[j - coins[i]];
AC代码
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]];
}
}
return dp[amount];
}
};
这里要注意一下:外层循环是遍历的物品数,而内层循环遍历的是背包的容量,那么能够将他们反过来吗?这就引出了下面这道题
题目链接:leetcode377.组合总和Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
示例 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:
示例 2:
输入:nums = [9], target = 3
输出:0
首先由示例1中就可以看出1 1 2,2 1 1 ,1 2 1都算不同的结果,因此和数字的位置有关,究其本质,这是一道排列的题,个数可以不限使用,说明这是⼀个完全背包问题。
那么如果使用题目一中先遍历物品再遍历背包容量的思想能够得到正确答案吗?显然不能
如果先遍历物品,即遍历物品的for循环在外层,那么试想一下,每个下标为i + 1
的物品只能放到i
物品的后面,则就失去了排列的定义,我们想要后面的物品既可以放到当前物品的后面,也可以放到当前物品的前面,这就需要先遍历背包容量,再遍历物品。
AC代码:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1,0);
dp[0] = 1;
for(int j = 1;j<=target;j++)
{
for(int i = 0;i<nums.size();i++)
{
//因为可能存在两个数相加超过int的数据,所以需要在if⾥加上dp[i] < INT_MAX - dp[i - num]
if(j - nums[i] >= 0 && dp[j] < INT_MAX - dp[j - nums[i]])
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
到这里就能得出一个重要的结论:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
题目链接:leetcode70.爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例1
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例2
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
这道题使用斐波那契树的思想解决当然是可以的,而且这也是最简单的思想,这里不再多说,代码如下:
class Solution {
public:
int climbStairs(int n) {
if(n == 0 || n == 1) return 1;
if(n == 2) return 2;
vector<int> dp(n + 1,0);
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
if(n >= 3)
{
for(int i = 3;i<=n;i++)
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
但是如果题目改成你每次可以跳m个台阶,1
因此代码如下:
class Solution {
public:
int climbStairs(int n) {
vector<int> dp(n + 1,0);
dp[0] = 1;
for(int i = 1;i<=n;i++)
{
//这里给的是m,如果改为2则是上面题目的答案
for(int j = 1;j<=m;j++)
{
if(i - j >= 0) dp[i] +=dp[i - j];
}
}
return dp[n];
}
};
题目链接:leetcode322.零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
由于这道题只要求得到能够凑成目标价值的最小硬币数目,因此和组合,排列无关,则for循环的先后顺序也无所谓。
首先定义dp数组,dp[j]表示凑成价值为j的最少硬币数。
那么dp[j]和coins的关系就不难推出,dp[j] = min(dp[j],dp[j - coins[i]] + 1)。
假设j = 5,coins[i] = 3,那么我只需要知道dp[2]是多少后,通过dp[2] + 1(即得到价值为2的数目再加上3就能得到价值为5的硬币的数目)和dp[j](j = 5)取最小即可。
接下来就是初始化,如果初始化为0,那么0永远是最小的,因此它会覆盖后面全部的最小值,所以初始化要无限大,因此可以初始化为INT_MAX
注意:dp[0]=0,因为凑⾜总价值为0所需硬币的个数⼀定是0
AC代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1,INT_MAX);
dp[0] = 0;
//进一步初始化,每一个coins[i]都可以以1个硬币组成
for(int i = 0;i<coins.size();i++)
{
if(coins[i] <= amount)
dp[coins[i]] = 1;
}
for(int i = 0;i<coins.size();i++)
{
for(int j = coins[i];j<=amount;j++)
{
//排除dp[j - coins[i]]为INT_MAX的情况
if(dp[j - coins[i]] != INT_MAX)
dp[j] = min(dp[j],dp[j - coins[i]] + 1);
}
}
//判断能否组成amount
return dp[amount] == INT_MAX ?-1 : dp[amount];
}
};
题目链接:leetcode279.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
这道题和上道题的思想一样,无非就是物品我们得自己找。而物品又都是完全平方数来组成的,那么我们只需要找到,比n小于等于的那个完全平方数,则该完全平方数之前的完全平方数才能构成n。
首先定义dp数组,dp[j]表示构成j的最小完全平方数的个数。
我们定义一个num = sqrt(n)
,则只要i<=num
,则i*i
就可能会构成j
那么dp[j]和完全平方数(i*i
)的状态转移方程为:
dp[j] = min(dp[j],dp[j - i * i] + 1)
接下来就是初始化,如果初始化为0,那么0永远是最小的,因此它会覆盖后面全部的最小值,所以初始化要无限大,因此可以初始化为INT_MAX
这里dp[0]要初始化为0,因为题中说的完全平方数是从1,4,9…开始的,所以dp[0]=0,这也是为了方便后面的递推公式
AC代码
class Solution {
public:
int numSquares(int n) {
int num = sqrt(n);//获取num
vector<int> dp(n + 1,INT_MAX);
dp[0] = 0;
for(int i = 1;i<=num;i++)
{
for(int j = i*i;j<=n;j++)
{
//排除为INT_MAX的情况
if(dp[j - i*i] != INT_MAX)
dp[j] = min(dp[j],dp[j - i*i] + 1);
}
}
return dp[n];
}
};