文章目录
- 背包问题
- [1. 01背包问题](#1. 01背包问题)
-
- [1.1 思路](#1.1 思路)
-
- [1.1.1 dp数组定义](#1.1.1 dp数组定义)
- [1.1.2 递归公式](#1.1.2 递归公式)
- [1.1.3 初始化](#1.1.3 初始化)
- [1.2 代码](#1.2 代码)
- [1.3 优化(滚动数组)](#1.3 优化(滚动数组))
- [1.4 类似题目](#1.4 类似题目)
-
- [[416. 分割等和子集 - 力扣(LeetCode)](https://leetcode.cn/problems/partition-equal-subset-sum/description/)](#416. 分割等和子集 - 力扣(LeetCode))
- [[1049. 最后一块石头的重量 II - 力扣(LeetCode)](https://leetcode.cn/problems/last-stone-weight-ii/description/)](#1049. 最后一块石头的重量 II - 力扣(LeetCode))
- [[494. 目标和 - 力扣(LeetCode)](https://leetcode.cn/problems/target-sum/description/)](#494. 目标和 - 力扣(LeetCode))
- [[474. 一和零 - 力扣(LeetCode)](https://leetcode.cn/problems/ones-and-zeroes/description/)](#474. 一和零 - 力扣(LeetCode))
- [2. 完全背包问题](#2. 完全背包问题)
-
- [2.1 思路](#2.1 思路)
- [2.2 代码](#2.2 代码)
- [2.3 相关题目](#2.3 相关题目)
-
- [[518. 零钱兑换 II - 力扣(LeetCode)](https://leetcode.cn/problems/coin-change-ii/submissions/)](#518. 零钱兑换 II - 力扣(LeetCode))
- [[377. 组合总和 Ⅳ - 力扣(LeetCode)](https://leetcode.cn/problems/combination-sum-iv/description/)](#377. 组合总和 Ⅳ - 力扣(LeetCode))
- [[57. 爬楼梯(第八期模拟笔试)](https://kamacoder.com/problempage.php?pid=1067)](#57. 爬楼梯(第八期模拟笔试))
- [[322. 零钱兑换 - 力扣(LeetCode)](https://leetcode.cn/problems/coin-change/description/)](#322. 零钱兑换 - 力扣(LeetCode))
- [[279. 完全平方数 - 力扣(LeetCode)](https://leetcode.cn/problems/perfect-squares/description/)](#279. 完全平方数 - 力扣(LeetCode))
- [[139. 单词拆分 - 力扣(LeetCode)](https://leetcode.cn/problems/word-break/)](#139. 单词拆分 - 力扣(LeetCode))
- [3. 多重背包问题](#3. 多重背包问题)
-
- [3.1 **思路:**](#3.1 思路:)
- [3.2 代码](#3.2 代码)
- [4. 总结](#4. 总结)
-
- [4.1 递推公式](#4.1 递推公式)
-
- [4.1.1 问能否能装满背包(或者最多装多少)](#4.1.1 问能否能装满背包(或者最多装多少))
- [4.1.2 问装满背包有几种方法](#4.1.2 问装满背包有几种方法)
- [4.1.3 问背包装满最大价值](#4.1.3 问背包装满最大价值)
- [4.1.4 问装满背包所有物品的最小个数](#4.1.4 问装满背包所有物品的最小个数)
- [4.2 遍历顺序](#4.2 遍历顺序)
-
- [4.2.1 01背包](#4.2.1 01背包)
- [4.2.2 完全背包](#4.2.2 完全背包)
背包问题

- 完全背包: 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
- 01背包: 完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
- 多重背包: 转化成01背包
1. 01背包问题
1.1 思路
1.1.1 dp数组定义
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
1.1.2 递归公式
-
不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。
-
放物品i:背包空出物品i的容量后,背包容量为
j - weight[i],dp[i - 1][j - weight[i]]为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i](物品i的价值),就是背包放物品i得到的最大价值
递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
1.1.3 初始化
当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
1.2 代码
cpp
#include <stdio.h>
#include <iostream>
#include <vector>
int main(){
using namespace std;
int m,n;
cin>>m>>n;
vector<int> value(m,0);
std::vector<int> weight(m,0);
for(int i=0;i<m;i++){
cin>>weight[i];
}
for(int i=0;i<m;i++){
cin>>value[i];
}
// dp[i][j]代表行李箱空间位j情况下,从[0,i]的物品取能达到的最大价值
vector<vector<int>> dp(m,vector<int>(n+1,0));
// 初始化,转移式用到dp[i-1]
// j<weight[0] 已经在上方被初始化0
//
for(int j = weight[0];j<=n;j++){
dp[0][j] = value[0]; // 初始化第一行,只取[0,0]物品放入j容量的背包的最大价值
}
// 一行一行的填充dp数组
for(int i=1;i<m;i++){ // 遍历物品
for(int j = 0;j<=n;j++){ // 遍历背包容量
// dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
if(j<weight[i]){
dp[i][j] = dp[i-1][j];
// 背包容量小于weight[i],装不下这个物品i,则只能在[0,i-1]物品选择最大价值
}
else{
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
// 装的下该物品i
// 1. 留出weight[i]空间,装入物品[i], 最大价值为dp[i-1][j-weight[i]]+value[i]
// 在[0,i-1]选装入j-weight[i],再装入物品i
// 2. 不装该物品 dp[i-1][j]
}
}
}
cout << dp[m - 1][n] << endl;
return 0;
}
1.3 优化(滚动数组)
递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
使用一维数组定义,dp[j]表示容量j的背包所背的最大价值
得到一维递推公式 ,dp[j] = max(dp[j],dp[j-weight[i]]+value[i]
初始化 dp[0] = 0;
cpp
#include <stdio.h>
#include <iostream>
#include <vector>
int main(){
using namespace std;
int m,n;
cin>>m>>n;
vector<int> value(m,0);
std::vector<int> weight(m,0);
for(int i=0;i<m;i++){
cin>>weight[i];
}
for(int i=0;i<m;i++){
cin>>value[i];
}
// dp[j]代表行李箱空间位j情况下,能达到的最大价值
vector<int> dp(n+1,0);
for(int i=0;i<m;i++){ // 遍历物品
for(int j = n;j>=weight[i];j--){ // 遍历背包容量
// 这里遍历背包容量时,不可以从前往后
// 对比二维dp数组,从前往后遍历时,使用dp[i-1][*]计算,
// 一维数组从前往后使用可以认为使用dp[i][*]!=dp[i-1][*],
// 从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}
}
cout << dp[n] << endl;
return 0;
}
1.4 类似题目
416. 分割等和子集 - 力扣(LeetCode)
给你一个 只包含正整数的非空数组
nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路:
cpp
// 1. 背包的体积为sum / 2
// 2. 背包要放入的商品(集合里的元素),重量为元素的数值,价值也为元素的数值
// ** 容量j的背包最大装下j的数值
// 3. 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
// 4. 背包中每一个元素是不可重复放入。
1049. 最后一块石头的重量 II - 力扣(LeetCode)
有一堆石头,用整数数组
stones表示。其中stones[i]表示第i块石头的重量。每一回合,从中选出任意两块石头 ,然后将它们一起粉碎。假设石头的重量分别为
x和y,且x <= y。那么粉碎的可能结果如下:
如果
x == y,那么两块石头都会被完全粉碎;如果
x != y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y-x。最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回
0。
思路:
其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
494. 目标和 - 力扣(LeetCode)
给你一个非负整数数组
nums和一个整数target。向数组中的每个整数前添加
'+'或'-',然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。返回可以通过上述方法构造的、运算结果等于
target的不同 表达式 的数目。
思路:
-
分为两个集合,一部分
x取+,另外一部分取负号-(sum-x),有x-(sum-x)=target,x=(target+sum)/2,问题等价求 用nums装满容量为x的背包,有几种方法,数字价值和容量相同。 -
dp数组,
dp[i][j],表示用[0,i]下标的数字,装满x背包的方法数目 -
递推公式:
-
不放物品i:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。
-
放物品i: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; -
-
初始化: 当前值 是由上方和左上方推出。所以需要初始化
dp[0][j]和dp[i][0]
474. 一和零 - 力扣(LeetCode)
给你一个二进制字符串数组
strs和两个整数m和n。请你找出并返回
strs的最大子集的长度,该子集中 最多 有m个0和n个1。如果
x的所有元素也是y的元素,集合x是集合y的 子集 。
思路:
cpp
// dp[i][j]表示用最多i个0,和j个1的最大子集长度
// dp[k][i][j]表示用字符串[0,k], 最多i个0和j个1的最大子集合长度
// 递推公式
// 1. 选取该字符串 dp[i - count[k][0]][j - count[k][1]] + 1
// 2. 不选取该字符串 dp[i][j]
dp[i][j] = max(dp[i - count[k][0]][j - count[k][1]] + 1, dp[i][j]);
dp[k][i][j] = max(dp[k-1][i-count[k][0]][j - count[k][1]]+1, dp[k-1][i][j]);
// 遍历背包容量且从后向前遍历!
// 从后往前遍历的原因是
// 这里的[i][j]表示背包的两个维度,dp数组没有[k]这个维度
// 这等价于基础01背包问题中的一维dp数组方法
// 字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。
// 初始化: 因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
2. 完全背包问题
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
2.1 思路
使用一维dp数组解决01背包问题,提到01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
cpp
// 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]);
}
}
// 完全背包核心代码
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]);
}
}
遍历顺序问题
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
2.2 代码
一维dp数组
cpp
#include <iostream>
#include <vector>
int main(){
using namespace std;
int n,v;
cin >> n>> v;
vector<int> weight(n,0);
vector<int> value(n,0);
for(int i = 0;i<n;i++){
cin>>weight[i]>>value[i];
}
vector<int> dp(v+1,0);
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]]+value[i]);
}
}
cout<<dp[v];
}
2.3 相关题目
518. 零钱兑换 II - 力扣(LeetCode)
给你一个整数数组
coins表示不同面额的硬币,另给一个整数amount表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回
0。假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
思路:
求装满背包有几种方法,公式都是:dp[j] += dp[j - nums[i]];
类似题目目标和 - 力扣(LeetCode);
注意遍历顺序,先遍历物品再遍历背包容量得到的是组合数 ,反之得到的是排列数
代码:
cpp
// 一维dp数组
class Solution {
public:
int change(int amount, vector<int>& coins) {
int len = coins.size();
// dp[j] 表示组成j的方法数
vector<uint64_t> dp(amount + 1, 0);
dp[0] =1 ;
for (int i = 0; i < len; i++) {
for (int j = coins[i]; j < amount + 1; j++) {
dp[j] = dp[j] + dp[j - coins[i]];
}
}
return dp[amount];
}
};
// 二维dp数组
class Solution {
public:
int change(int amount, vector<int>& coins) {
int len = coins.size();
// dp[i][j] 表示前i个能组成j的方法数
vector<vector<uint64_t>> dp(len + 1, vector<uint64_t>(amount + 1, 0));
for (int i = 0; i < len + 1; i++) {
dp[i][0] = 1;
}
for (int i = 1; i < len + 1; i++) {
for (int j = 1; j < amount + 1; j++) {
dp[i][j] = dp[i - 1][j]; // 不选coins[i-1]
if (j >= coins[i - 1])
dp[i][j] += dp[i][j - coins[i-1]]; // 选取coins[i]
}
}
return dp[len][amount];
}
};
377. 组合总和 Ⅳ - 力扣(LeetCode)
给你一个由 不同 整数组成的数组
nums,和一个目标整数target。请你从nums中找出并返回总和为target的元素组合的个数。题目数据保证答案符合 32 位整数范围。
请注意,顺序不同的序列被视作不同的组合。
思路:
与零钱兑换 II - 力扣(LeetCode)不同的是,这题求的是排列数,故需要调换循环遍历的顺序
57. 爬楼梯(第八期模拟笔试)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
思路:
与组合总和 Ⅳ - 力扣(LeetCode)相同,这题也是求排列数,其中nums[i] = i+1;
322. 零钱兑换 - 力扣(LeetCode)
给你一个整数数组
coins,表示不同面额的硬币;以及一个整数amount,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回
-1。你可以认为每种硬币的数量是无限的。
思路:
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);求min所以需要初始为INT32_MAX;
凑足总金额为0所需钱币的个数一定是0,所以dp[0]=0;
279. 完全平方数 - 力扣(LeetCode)
给你一个整数
n,返回 和为n的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,
1、4、9和16都是完全平方数,而3和11不是。
思路:
nums是完全平方数的数组,0到((int)sqrt(n))^2
139. 单词拆分 - 力扣(LeetCode)
给你一个字符串
s和一个字符串列表wordDict作为字典。如果可以利用字典中出现的一个或多个单词拼接出s则返回true。**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
思路:
- 确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
- 确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
dp[i] = dp[j] && st.count(s.substr(j, i - j))
- 初始化
dp[0]初始为true
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
- 确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
还要讨论两层for循环的前后顺序。本题一定是 先遍历 背包,再遍历物品。
3. 多重背包问题
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
3.1 思路:
多重背包问题转化为01背包问题
3.2 代码
cpp
#include <iostream>
#include <vector>
int main(){
using namespace std;
int c,n;
cin >> c>>n;
vector<int> weight(n,0);
vector<int> value(n,0);
vector<int> nums(n,0);
for(int i = 0;i<n;i++){
cin>>weight[i];
}
for(int i = 0;i<n;i++){
cin>>value[i];
}
for(int i = 0;i<n;i++){
cin>>nums[i];
}
vector<int> dp(c+1,0);
for(int i = 0;i<n;i++){
for(int j = c;j>=0;j--){ // 一维dp数组,注意遍历顺序
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {
dp[j] = max(dp[j], dp[j - k*weight[i]] + k*value[i]);
}
}
}
cout<<dp[c];
return 0;
}
4. 总结
4.1 递推公式
4.1.1 问能否能装满背包(或者最多装多少)
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
4.1.2 问装满背包有几种方法
dp[j] += dp[j - nums[i]] ;
4.1.3 问背包装满最大价值
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
4.1.4 问装满背包所有物品的最小个数
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
4.2 遍历顺序
4.2.1 01背包
二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
4.2.2 完全背包
纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。