题目来源
题目描述
题目解析
这是一个典型的背包问题:
- 对于每个物品,可以选择装/不装(决策,因此可以选择动态规划),每个物品最大只能装一次
- 要求:总重量不能超过m,求最大最装的重量
可以将每个物品的价值等价于它自己的重量
动态规划
思路
(1)确定状态
- 最后一步:最后一个物品(重量为
A
n
−
1
A_{n-1}
An−1)是否装入背包(不要去想最终方案会不会装入背包,反正肯定需要遍历数组),假设放入当前物品之前的重量为
W
n
−
2
W_{n-2}
Wn−2
- 情况一:不装入,重量不变,为 W n − 2 W_{n-2} Wn−2
- 情况二:装入,重量变为 W n − 2 + A n − 1 W_{n-2}+ A_{n-1} Wn−2+An−1
- 取上面两种决策中重量的那个
- 装入或者不装入取决于当前物品和剩余重量的关系
- 子问题:
- 要求前n个物品对于背包w能够获得的最大重量
- 先要求前n-1个物品对于背包w能够获得的最大重量
- 状态:
- 上面有三个变化维度:第i个物品,背包容量j,能够获得的最大重量
- 因此
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示前i个物品装入大小为j的背包中,可以获得的最大重量
- d p [ a . s i z e ( ) ] [ i ] dp[a.size()][i] dp[a.size()][i]最后一个物品[0…a.size() - 1]
- d p [ i ] [ j ] dp[i][j] dp[i][j]前i个物品 [ 0..... i − 1 ] [0.....i-1] [0.....i−1]
- …
- dp[2][i] 前2个物品[0…1]
- dp[1][j] 前1个物品[0…0]
- dp[0][j] 前0个物品[0…-1]
- 也就是说,dp的第一维长度为a.size() + 1,第二维长度为w+1(物品重量+1),结果为最大重量(int类型)
- 注意这里是前 i 件物品,因此我们当前面对的物品下标是 i - 1,重量为A[i-1],注意,在所有背包问题中,都是用的这种 ”前 i 项“ ,而不是 ”第 i 项“ 来表示状态,这是因为 ”第 0 项“ 没法表示背包为空的情况。
(2)转移方程
- 对于dp[i][j]表示前i个物品装入大小为j的背包中,可以获得的最大重量
- 那么对于前i个物品的最后一个物品,也就是第i-1个物品,是否放入背包
- 如果当前物品重量大于背包容量
A
i
−
1
>
j
A_{i-1} > j
Ai−1>j,只有一种决策
- 不能放入,因此最大重量为 考 虑 前 i − 1 个 物 品 , 背 包 容 量 为 j 的 最 大 重 量 考虑前i-1个物品,背包容量为j的最大重量 考虑前i−1个物品,背包容量为j的最大重量,也即是 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i−1][j]
- 如果当前物品重量小于等于背包容量
A
i
−
1
>
j
A_{i-1} > j
Ai−1>j,两种决策:
- 不放入: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i−1][j]
- 放入:由于背包已经占用了 A[i-1] 的重量,我们要找的就是在剩余容量下的最大装载量,也就是说: d p [ i ] [ j ] = d p [ i − 1 ] [ j − A i − 1 ] + A i − 1 dp[i][j] = dp[i-1][j - A_{i-1}] + A_{i-1} dp[i][j]=dp[i−1][j−Ai−1]+Ai−1
- 两种决策中选择一种价值最大的
- 如果当前物品重量大于背包容量
A
i
−
1
>
j
A_{i-1} > j
Ai−1>j,只有一种决策
(3)初始情况和边界条件
- 初始情况:
- dp[0][j] = 0 也就是没有物品时(前0个物品),获得的最大重量是0
- dp[i][0] = 0 也就是背包容量为0时,能够获得的最大重量也是0
(4)遍历顺序
- 两个维度,双重循环
- 先初始化dp[0][j]为0
- 然后从上到下[1…a.size() - 1]、从左到右[1…w]
- 答案是dp[a.size()][w]:表示,对于数组个物品,装入w背包的能够获得的最大重量
(5)举个例子
- arr = {3,4,8,5}, w = 10
class Solution {
public:
int backPack(int m, vector<int> &a) {
int n = a.size();
if(m == 0 || n == 0){
return 0;
}
std::vector<std::vector<int >> dp(n +1, std::vector<int>(m + 1));
for (int i = 0; i < n + 1; ++i) { // 前i个物品
for (int j = 0; j < m + 1; ++j) {
if(i == 0 || j == 0){
dp[i][j] = 0; //没有物品 || 背包承重为0
}else if(j - a[i - 1] >= 0){ // 放得下
dp[i][j] = std::max(
dp[i - 1][j],
dp[i - 1][j - a[i - 1]] + a[i - 1]
);
}else{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][m];
}
};
class Solution {
public:
int backPack(int m, vector<int> &a) {
int n = a.size();
if(m == 0 || n == 0){
return 0;
}
std::vector<std::vector<int >> dp(2, std::vector<int>(m + 1));
for (int i = 0; i < n + 1; ++i) { // 前i个物品
for (int j = 0; j < m + 1; ++j) {
if(i == 0 || j == 0){
dp[i % 2][j] = 0; //没有物品 || 背包承重为0
}else if(j - a[i - 1] >= 0){ // 放得下
dp[i % 2][j] = std::max(
dp[(i - 1) % 2][j],
dp[(i - 1) % 2][j - a[i - 1]] + a[i - 1]
);
}else{
dp[i % 2][j] = dp[(i - 1) % 2][j];
}
}
}
return dp[n % 2][m];
}
};
class Solution {
public:
/**
* @param m: An integer m denotes the size of a backpack
* @param a: Given n items with size A[i]
* @return: The maximum size
*/
int backPack(int m, vector<int> &a) {
int len = a.size();
if(m == 0 || len == 0){
return 0;
}
std::vector<std::vector<int>> dp(len + 1 , std::vector<int>(m + 1));
for (int i = 1; i <= len; ++i) { // 当放第1个到第A.length-1个物品时
for (int j = 1; j <= m; ++j) { // 遍历重量
if(j >= a[i - 1]){
//若该物品所占空间小于等于背包总空间,则需将背包空间腾出至少A[i - 1]后,将该物品放入。
//放入新物品后背包最大可装满空间可能更大,也可能变小大,
// 取大值作为背包空间为j且放第i个物品时可以有的最大可装满空间。
dp[i][j] = std::max(
dp[i - 1][j - a[i - 1]] + a[i - 1], // 放
dp[i - 1][j]); // 不放
}else{
dp[i][j] = dp[i - 1][j]; //背包可装满的最大空间不变
}
}
}
return dp[len][m];
}
};
class Solution {
public:
/**
* @param m: An integer m denotes the size of a backpack
* @param a: Given n items with size A[i]
* @return: The maximum size
*/
int backPack(int m, vector<int> &a) {
// 如果背包容量或者物品数量为0,则直接返回
int n = a.size();
if(m == 0 || n == 0){
return 0;
}
std::vector<std::vector<int>> dp(n , std::vector<int>(m + 1));
// 初始化第一行
for (int i = 0; i < m + 1; ++i) {
// 枚举当前背包容量是否可以放下第一个物品
dp[0][i] = i >= a[0] ? a[0] : 0;
}
// 之后的每一个物品都可以根据前一个物品的状态求出来
for (int i = 1; i <= n; ++i) { //遍历每一个物品
for (int j = 0; j <= m; ++j) { // 遍历每一个重量
int unsel = dp[i - 1][j]; // 当前物品不放入
// 选择当前物品:前提是背包容量能放下这件物品
int sel = j >= a[i] ? dp[i - 1][j - a[i]] + a[i] : 0;
dp[i][j] = std::max(unsel, sel);
}
}
return dp[n - 1][m];
}
};
滚动数组优化:
class Solution {
public:
/**
* @param m: An integer m denotes the size of a backpack
* @param a: Given n items with size A[i]
* @return: The maximum size
*/
int backPack(int m, vector<int> &a) {
// 如果背包容量或者物品数量为0,则直接返回
int n = a.size();
if(m == 0 || n == 0){
return 0;
}
std::vector<std::vector<int>> dp(2 , std::vector<int>(m + 1));
// 初始化第一行
for (int i = 0; i < m + 1; ++i) {
// 枚举当前背包容量是否可以放下第一个物品
dp[0][i] = i >= a[0] ? a[0] : 0;
}
// 之后的每一个物品都可以根据前一个物品的状态求出来
for (int i = 1; i <= n; ++i) { //遍历每一个物品
for (int j = 0; j <= m; ++j) { // 遍历每一个重量
int unsel = dp[(i - 1) & 1][j]; // 当前物品不放入
// 选择当前物品:前提是背包容量能放下这件物品
int sel = j >= a[i] ? dp[(i - 1) & 1][j - a[i]] + a[i] : 0;
dp[i&1][j] = std::max(unsel, sel);
}
}
return dp[(n - 1)&1][m];
}
};
思路
- 所有的背包问题,先从重量入手,已知一个背包的最大承重是 m m m,不管什么装,每个装物品的方案的总重量只能从 0 − m 0-m 0−m中选择
- 那我们看每一个重量能不能装满就可以了
(1)确定状态
- 最后一步:前n个物品的最后一个物品(重量为
A
n
−
1
A_{n-1}
An−1)是否装入背包
- 情况一:如果前 n − 1 n-1 n−1个物品能够拼出 m m m,那么前 n n n个物品也能拼出 m m m
- 情况二:如果前 n − 1 n-1 n−1个物品能够拼出 m − A n − 1 m - A_{n-1} m−An−1,那么最后加上物品 A n − 1 A_{n-1} An−1,拼出 m m m
- 子问题:
- 要求前 n n n个物品能否拼出重量 0..... m 0.....m 0.....m
- 先要求出前n-1个物品能否拼出 0.... m 0....m 0....m
- 状态:
- 上面有变化维度:第i个物品,重量j,能否拼出
- dp[i][j]表示对于前i个物品,能否拼出重量j( t r u e / f a l s e true/false true/false)
(2)转移方程
- 对于dp[i][j]
- 不能装,那么取决于前i-1个物品能否拼出 j j j: d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i - 1][j] dp[i][j]=dp[i−1][j]
- 能装(
A
i
−
1
<
=
j
A_{i-1} <= j
Ai−1<=j)
- 如果 A i − 1 = = j A_{i-1} == j Ai−1==j,能拼: d p [ i ] [ j ] = t r u e dp[i][j] = true dp[i][j]=true
- 如果 A i − 1 < j A_{i-1} < j Ai−1<j,取决于前i-1个物品能否拼出 j − A i − 1 j - A_{i-1} j−Ai−1: d p [ i ] [ j ] = d p [ i − 1 ] [ j − A i − 1 ] dp[i][j] = dp[i-1][j - A_{i-1}] dp[i][j]=dp[i−1][j−Ai−1]
(3)初始条件&&边界情况
- 初始条件:
- dp[0][0] = true,前0个物品也就是没有物品时,有一种方式能拼出重量0
- dp[0][1…m]=false,0个物品不能拼出大于0的重量
- dp[1…n][0] = true, 当有物品时,能拼出物品为0的方案
- 边界情况:
- 无
(4)计算顺序
- 从左到右
- 从上到下
- 答案是dp[i][j]右下角最后为true为那个j
class Solution {
public:
int backPack(int m, vector<int> &a) {
int n = a.size();
if(m == 0 || n == 0){
return 0;
}
std::vector<std::vector<bool >> dp(n +1, std::vector<bool>(m + 1));
dp[0][0] = true;
for (int i = 1; i < n + 1; ++i) { // 前i个物品
dp[i][0] = true;
for (int j = 1; j < m + 1; ++j) { //对于重量[1...m], 能否装满
// 前i个物品
if(a[i - 1] > j){ // 当物品重量 > 背包重量时
dp[i][j] = dp[i - 1][j];
}else if(a[i - 1] == j){
dp[i][j] = true;
}else{
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - a[i - 1]];
}
}
}
int res = 0;
for (int sum = m; sum >=0; --sum) {
if (dp[n][sum]) {
return sum;
}
}
return 0;
}
};
一维数组优化
class Solution {
public:
/**
* @param m: An integer m denotes the size of a backpack
* @param a: Given n items with size A[i]
* @return: The maximum size
*/
int backPack(int m, vector<int> &a) {
// 如果背包容量或者物品数量为0,则直接返回
int n = a.size();
if(m == 0 || n == 0){
return 0;
}
// 对于每一个背包,能不能把它装满
vector<int> dp(m+1,0);
for (int i = 0; i < n; ++i) { // 对于每一个物品
for (int j = m; j >= 0; --j) { // 计算第二行时,因为会用到第一行前面的数据,因此从第一行尾端往前端进行覆盖
int sel = dp[j]; // 放入
int unsel = dp[j - a[i]] + a[i]; // 不放入
dp[j] = max(sel, unsel);
}
}
return dp[m];
}
};
记忆化搜索
- 如果要我们设计一个DFS函数对所有的方案进行枚举的话,大概是这么一个函数签名:
int dfs(int[] A, int idx, int letfM, int totalM);
- 其中A和[输入的物品重量]、totalM表示[背包总重量],属于不变参数,而idx和letfM分别代表[当前枚举到哪件物品]和[现在的剩余容量]
- 要求返回最大重量
递归三部曲:
- 肯定需要枚举每一个物品,当枚举完所有的的物品(idx >= a.size())时,返回0
- 对于每一个物品,有两种选择
- 当[物品重量]大于[剩余容量],只有一种决策,那就是不放入
- 当[物品重量]小于等于[剩余容量],那么有两种决策,放入 or 不放入,两种决策中选择一种重量较大的