完全背包理论基础
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次),**求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
01背包和完全背包唯一不同就是体现在遍历顺序上,完全背包是从小到大遍历,并且两个for循环的先后循环顺序不影响计算dp[j]:
// 先遍历物品,再遍历背包
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]);
}
}
// 先遍历背包,再遍历物品
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]);
}
}
但是:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
多重背包理论基础
题目1:518. 零钱兑换Ⅱ
完全背包是能能否凑成总金额,本题则要求凑成总金额的组合数
注意是组合数,不是排列数(组合不强调元素之间的顺序,排列强调元素之间的顺序。)。
一维dp数组
1.确定dp数组以及下标的含义
dp[j]:凑成总金额j的硬币组合数为dp[j]
2.确定状态转移方程
dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。
所以递推公式:dp[j] += dp[j - coins[i]];
3.dp数组初始化
dp[0]=1,凑成总金额0的货币组合数为1。
并且dp[0]一定要为1,dp[0] = 1是 递归公式的基础。
下标非0的dp[j]初始化为0,和01背包一样。
4.确定遍历顺序
求组合数就是外层for循环遍历物品,内层for遍历背包。
5.举例推导dp数组
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];
}
};
题目2:377. 组合总和 Ⅳ
这一题和518相比,其实就是一个求组合一个求排列
那么更换下遍历顺序:求排列数就是外层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] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) { //测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
题目3:70. 爬楼梯(进阶)
将:每次你可以爬1或2个台阶
改为:每次可以爬1个台阶、2个台阶、3个台阶…直到n个台阶,问有多少种不同的方法可以爬到楼顶?
改动后题目的解题思路其实和377类似,都是求有多少种排列
dp[j]表示爬到有i个台阶的楼顶有dp[j]种方法
class Solution {
public:
int climbStairs(int n) {
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];
}
}
return dp[n];
}
};
题目4:322. 零钱兑换
一维dp数组
1.确定dp数组以及下标的含义
dp[j]:凑成总金额j的最少硬币数为dp[j]
2.确定状态转移方程
如果递推到dp[i],或者说如何由前面的状态得到dp[i]
dp[i]前面的状态就是由dp[j - coins[i]]得到,以coins=[1,2,5]为例,dp[i]的前面状态为dp[j-1]、dp[j-2]、dp[j-5]
前面的状态dp[j-1]、dp[j-2]、dp[j-5]加一个硬币就可以得到dp[j],比如dp[j-5]加一个面额为5的硬币就可以得到dp[i]
并且dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的
所以递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
3.dp数组初始化
dp[0]=0,凑成金额0所需要的硬币个数为0。
考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
4.确定遍历顺序
求硬币的最小个数,不是组合也不是排序,因此怎么遍历都可以
5.举例推导dp数组
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0; //面额0只需要0个硬币
for (int i = 0; i < coins.size(); i++) { //遍历物品
for (int j = coins[i]; j <= amount; j++) { //遍历背包
if (dp[j - coins[i]] != INT_MAX) { //如果dp[j - coins[i]]是初始值则跳过,即前面dp值在有计算过的基础上才能转移
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) { //遍历背包
for (int j = 0; j < coins.size(); j++) { //遍历物品
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX) { //需要凑的金额大于硬币价值
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
题目5:279.完全平方数
力扣超话来的,谁是完全背包?
n就是背包容量,然后给了若干个数字(无限次使用),求装满容量n的背包(凑出目标值n)所需要用到的是最少数字个数是多少。
只不过这题将数字改成了数字的完全平方
1.确定dp数组以及下标的含义
dp[j]:凑成目标值i最少需要dp[j]个数的完全平方
2.确定状态转移方程
dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。
此时我们要选择最小的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);
原题同322
3.dp数组初始化
dp[0]=0,和为0的完全平方数的最小数量为0。
从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖
4.确定遍历顺序
不是组合也不是排序,因此怎么遍历都可以
5.举例推导dp数组
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i <= n; i++) { // 遍历背包
for (int j = 1; j * j <= i; j++) { // 遍历物品
dp[i] = min(dp[i - j * j] + 1, dp[i]);
}
}
return dp[n];
}
};
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i * i <= n; i++) { // 遍历物品
for (int j = 1; j <= n; j++) { // 遍历背包
if (j - i * i >= 0) {
dp[j] = min(dp[j - i * i] + 1, dp[j]);
}
}
}
return dp[n];
}
};
题目6:139.单词拆分
本题怎么转换成背包问题其实不难看出
字符串s就是背包,字典里的单词就是物品,
单词能否组成字符串,就是物品能否装满背包
1.确定dp数组及下标的含义
dp[i] : 长度为i的字符串能够拆分成单词,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
2.确定状态转移方程
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。
3.dp数组初始化
dp[0]=true
从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
4.确定遍历顺序
都可以
5.举例推导dp[i]数组
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end()); //为了快速判断字符串s拆分出来的子串在wordDict中是否出现,用哈希表存储worddict中的单词
vector<bool> dp(s.size() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) { //遍历背包
for (int j = 0; j < i; j++) { //遍历物品
string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end() && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
};