一、完全背包写法思路
完全背包和01背包在题目要求上唯一的的区别就是,01背包每个物品只能用一次,而完全背包可以用无数次。
在写法上面,01背包和完全背包唯一不同就是体现在遍历顺序上。
首先再回顾一下01背包的核心代码:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
01背包问题中,遍历背包容量的时候,是从后往前、从大到小遍历的。 而且在一维滚动数组的写法里,必须先遍历物品种类,再遍历背包容量。 不能颠倒!!!
在完全背包问题里,由于物品是可以添加多次的,是允许重复的,所以,遍历背包容量的时候,就要从前往后、从小到大遍历了。 即:
// 先遍历物品,再遍历背包
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]);
}
}
那么在完全背包中,遍历物品种类和背包容量的顺序应该是什么呢?
其实对于纯完全背包问题,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!。纯完全背包的意思就是,给定一个背包容量,然后求背包最大能装的价值是多少。就是单纯求值。因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!
但是有两种情况是特殊的:一是求组合问题,而是求排列问题。
组合问题要求不能有顺序,排列问题要求必须有顺序。
如果是组合问题,那么就是先遍历物品再遍历背包容量。如果是求解排列问题,就先遍历容量再遍历物品
二、经典题目剖析
2.1 零钱兑换 II
题目链接: 518. 零钱兑换 II - 力扣(LeetCode)
题目要求: 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
解析:
可以取无限次,是完全背包问题,所以遍历背包容量的时候要从前往后遍历。因为是组合问题,所以要先遍历物品种类,再遍历背包。
class Solution {
public:
int change(int amount, vector<int>& coins) {
int size = coins.size();
vector<int>dp(amount + 1);//dp[j]的含义是,装满容量为j的袋子,装满它有多少种方法
dp[0] = 1;
//把dp[j]初始化为1,意思是装满容量为0的袋子有一种方法,就是什么都不装
for(int i = 0;i < size;i ++)
{
for(int j = coins[i];j <= amount;j ++)//由于是完全背包问题,所以要正序遍历
{
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
2.2 组合总和4
题目链接:377. 组合总和 Ⅳ - 力扣(LeetCode)
题目要求: 给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
解析:
完全背包问题,要求解的是排列数目,是排列问题,所以要先遍历背包容量,再遍历物品种类。
C++代码如下:
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
//元素都是不同的很关键
//有一说一,我觉得回溯也可以做(不知道会不会超时),我觉得很可能超时
//动态规划,排列做法
vector< long int>dp(target + 1);
dp[0] = 1;//初始化为1,没什么具体含义,就是为了做题的时候递推公式的推导符合要求,题目也保证target大于0了
for(int j = 0;j <= target;j ++)//因为这个题目要求解的是排列问题而不是组合问题或者是简单的完全背包问题,所以要把for循环的遍历顺序颠倒,先遍历背包容量再遍历物品
{
for(int i = 0;i < nums.size();i ++)
{
if(j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]]) //加上dp[j] < INT_MAX - dp[j - nums[i]]这个代码只是为了防止溢出,没什么意思,因为题目已经保证了结果不会溢出的,所以中间过程还会溢出就很离谱,妈的
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
2.3 零钱兑换
题目要求: 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
解析:
这道题不强调顺序,所以先遍历谁都无所谓。
主要是,由于要寻找最小的个数,所以初始化的时候要初始化为最大值,后面便于区分。
可以选择用当前硬币或者不用,递推公式为 dp[j] = min(dp[j],dp[j - coins[i]] + 1)。
如果dp[j - coins[i]] == INT_MAX就要跳过,因为根本凑不成j - coins[i]的钱,不能在此基础上再叠加了。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
//硬币是无限的,就是完全背包问题
//组合问题
vector<int>dp(amount + 1,INT_MAX);//dp[j]的含义就是组成金额是j的硬币最少个数
dp[0] = 0;
for(int j = 0;j <= amount;j ++)
{
for(int i = 0;i < coins.size();i ++)
{
if(coins[i] > 10000)//至超过了总金额的最大值,肯定用不到
continue ;
if(j >= coins[i] && dp[j - coins[i]] != INT_MAX)//这里很关键,要保证不是int_max的时候才能计算个数,不然就没有意义了,如果之前的位置是int——max,说明没有组合能组成那个数,那么也就不能递推了
{
;
}
}
}
if(dp[amount] == INT_MAX)
return -1;
return dp[amount];
}
};
2.4 单词拆分
题目要求: 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
解析:
可以重复使用,属于完全背包问题。求可能的排列,需要先遍历背包容量,再遍历物品。
dp[j]的含义就是,能不能拼出字符串s里面从第1个字符到第j个字符的那一部分.false就是不能,true就是能。
思路见注释:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//可以重复使用,属于完全背包问题
//求可能的排列,需要先遍历背包容量,再遍历物品
int size = s.size();
vector<bool>dp(size + 1,false);//dp[j]的含义就是,可以拼出字符串s里面从第1个字符到第j个字符的那一部分
dp[0] = true;//完全是为了推导,没啥具体含义
for(int j = 0;j <= size;j ++)//排列问题,先遍历背包容量
{
for(int i = 0;i < wordDict.size();i ++)
{
if(wordDict[i].size() > size)
continue ;
if(j >= wordDict[i].size() && dp[j - wordDict[i].size()] == true && wordDict[i] == s.substr(j - wordDict[i].size(),wordDict[i].size()))//还要保证字符片段相同
{
dp[j] = true;
}
}
}
return dp[size];
}
};
2.5 最低票价
题目链接: 983. 最低票价 - 力扣(LeetCode)
题目要求: 在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。
火车票有 三种不同的销售方式 :
一张 为期一天 的通行证售价为 costs[0] 美元; 一张 为期七天 的通行证售价为 costs[1] 美元; 一张 为期三十天 的通行证售价为 costs[2] 美元。 通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。
返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 。
解析:
这是一道稍微有点奇怪的完全背包问题。
dp[j]表示旅行到第j天的最低消费。
如果第i
天不需要出去旅行,既然不旅行,那么就不花钱,费用和之前一样,所以是dp[i]=dp[i-1]
如果第i
天需要出去旅行,那么有三种情况:
- 买第
i
天当天的票,花费costs[0]
元,即dp[i]=dp[i-1]+costs[0]
- 7天前买的票正好延续到今天,花费即为:
dp[i]=dp[i-7]+costs[1]
- 30天前买的票正好延续到今天,花费为:
dp[i]=dp[i-30]+costs[2]
然后取最小值作为最小花费。
C++代码如下:
class Solution {
public:
int mincostTickets(vector<int>& days, vector<int>& costs) {
//可以无限取用,是完全背包问题
int size = days.size();
int val = days[size - 1];
vector<int>dp(val + 1,0);//dp[j]表示旅行到第j天的最低消费
for(int i = 0;i < days.size();i ++)
{
dp[days[i]] = INT_MAX;//标记一下哪些位置是需要旅行的
}
for(int j = 1;j <= val;j ++)
{
if(dp[j] == 0){
dp[j] = dp[j - 1];//不需要旅行,那么费用不变
}else{
//这一天是需要旅行的,那就要把之前的钱都结算一下
int a = dp[j - 1] + costs[0];
int b = j > 7?dp[j - 7] + costs[1]:dp[0] + costs[1];
int c = j > 30?dp[j - 30] + costs[2]:dp[0] + costs[2];
dp[j] = min({a,b,c});//有三种选择
}
}
return dp[val];
}
};