目录
[01背包问题 二维](#01背包问题 二维)
[1. dp数组含义](#1. dp数组含义)
[2. 递推公式](#2. 递推公式)
[3. 初始化方式](#3. 初始化方式)
[4. 遍历顺序](#4. 遍历顺序)
[01背包问题 一维](#01背包问题 一维)
[1. dp数组dp[j]含义](#1. dp数组dp[j]含义)
[2. 递推公式](#2. 递推公式)
[3. 初始化方式](#3. 初始化方式)
[4. 遍历顺序](#4. 遍历顺序)
[416. 分割等和子集](#416. 分割等和子集)
背包问题
有一个重量数组记录第i个物品的重量;一个价值数组记录第i个物品的价值,物品数量为m;有一个背包容量n,问把m个物品任选放入容量为n的背包中,求最大价值
01背包问题 二维
动规四部曲:
1. dp数组含义
j容量的背包,从前i个物品任意选取放入,得到的最大价值
2. 递推公式
当前最大价值有两种情况:放第i个物品和不放第i个物品
不放第i个物品:dp[i-1][j];
放第i个物品:dp[i-1][j-weight[i]]+val[i]
解释:首先腾出放第i个物品的空间->+val[i],剩下的空间从前i-1个物品任取放入取最大价值->dp[i-1][j-weight[i]]
3. 初始化方式
首先定义dp数组时,初始化为0,覆盖了第一列;
然后处理第一行,仅第一个物品放入背包,如果容量够,值变成val[0]
这样做的目的是,因为递推是从左上到右下 ,所以要初始化第一行和第一列
4. 遍历顺序
先物品后背包/先背包后物品都可以
**先物品后背包:**遍历到第i个物品时,意味着从前i个物品中任选物品;然后从小到大遍历背包空间,依次看每种空间大小可以得到的最大价值
**先背包后物品:**遍历到空间大小为j时,然后遍历物品,遍历到第i个物品,就尝试把前i个物品任选放入这j个空间,看得到的最大价值
cpp
#include<iostream>
using namespace std;
#include<vector>
int main(){
int m,n;
cin>>m>>n;
vector<int> weight(m);
vector<int> val(m);
for(int i=0;i<m;i++){
cin>>weight[i];
}
for(int j=0;j<m;j++){
cin>>val[j];
}
//dp[i][j]含义:j容量的背包,从前i个物品任意选取放入,得到的最大价值
vector<vector<int>> dp(m,vector<int>(n+1,0));
//初始化dp数组:仅第一个物品放入背包,如果容量够,值变成val[0]
for(int j=weight[0]; j<=n; j++) dp[0][j] = val[0];
//递推公式:当前最大价值有两种情况:放第i个物品和不放第i个物品
//不放第i个物品:dp[i-1][j];
//放第i个物品:dp[i-1][j-weight[i]]+val[i]
//解释:首先腾出放第i个物品的空间->+val[i],剩下的空间从前i-1个物品任取放入取最大价值->dp[i-1][j-weight[i]]
//遍历顺序:先物品后背包/先背包后物品都可以
//先物品后背包:遍历到第i个物品时,意味着从前i个物品中任选物品;然后从小到大遍历背包空间,依次看每种空间大小可以得到的最大价值
//先背包后物品:遍历到空间大小为j时,然后遍历物品,遍历到第i个物品,就尝试把前i个物品任选放入这j个空间,看得到的最大价值
for(int i=1; i<m; i++){
for(int j=0; j<=n; j++){
if(j < weight[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+val[i]);
}
}
//打印dp数组
// for(int i=0; i<m; i++){
// for(int j=0; j<=n; j++){
// cout<<dp[i][j]<<" ";
// }
// cout<<endl;
// }
cout<<dp[m-1][n]<<endl;
return 0;
}
01背包问题 一维
一维压缩可行性分析
把二维dp压缩为一维dp,可行的原因是二维的递推公式:
不放第i个物品:dp[i-1][j];
放第i个物品:dp[i-1][j-weight[i]]+val[i]
仅依赖上层数据,所以只需维护一行数据(一个一维数组),在该数组上迭代更新即可
动规四部曲:
1. dp数组**dp[j]**含义
j容量的背包,从所有物品中(i个,但没体现在dp上)得任选放入,得到的最大价值
2. 递推公式
当前最大价值有两种情况:放第i个物品和不放第i个物品
不放第i个物品:dp[j];
放第i个物品:dp[j-weight[i]]+val[i]
解释:首先腾出放第i个物品的空间->+val[i],剩下的空间从前i-1个物品任取放入取最大价值->dp[j-weight[i]]
3. 初始化方式
对于背包问题一维dp,递推依赖上一循环的dp和第i个物品的重量,所以只需要将一维dp初始化为0即可开始迭代
4. 遍历顺序
先物品后背包+背包倒序遍历+背包从n遍历到weight[i]
**为什么倒序遍历:**因为递推公式会调用dp[j-weight[i]],如果正序遍历,则无法保证dp[j-weight[i]]是在多少个物品里面选择然后放入的,所以有可能出现重复放入同一物品的情况
举例:假设遍历到物品0,dp[1]容量够,给dp[1]赋了值,然后后面的大容量肯定也能放下物品0,所以会调用dp[1]的值再加上val[0],导致重复选取。
**为什么必须先物品后背包:**如果先背包后物品,由于背包需倒序,所以一开始是最大容量n,然后开始下一层遍历物品,假设前两个物品可以一起放下,那么开始执行循环逻辑,第一个物品可以放下,dp[n]更新为val[0];第二个物品也可以放下,dp[n] = max(dp[j], dp[j-weight[i]]+val[i]),也就是更新为物品0和物品1之间的最大值,(因为dp[j-weight[i]]还没有遍历到,一定是0)。
这样就导致只放下了物品0或者物品1,无法把两个物品都放入
**为什么背包从n遍历到weight[i]:**因为如果背包容量小于weight[i],第i个物品无需考虑放不放,因为一定放不下,直接保持原状就好,也就是不迭代j<weight[i]的部分。
cpp
#include<iostream>
using namespace std;
#include<vector>
int main(){
int m,n;
cin>>m>>n;
vector<int> weight(m);
vector<int> val(m);
for(int i=0;i<m;i++){
cin>>weight[i];
}
for(int j=0;j<m;j++){
cin>>val[j];
}
//dp[j]含义:j容量的背包,所有物品任选放入,得到的最大价值
vector<int> dp(n+1,0);
//初始化dp数组:直接初始化为0,这样就满足更新条件
for(int i=0; i<m; ++i){
for(int j=n; j>=weight[i]; --j){
dp[j] = max(dp[j], dp[j-weight[i]]+val[i]);
}
}
//打印dp数组
// for(int i=0; i<m; i++){
// for(int j=0; j<=n; j++){
// cout<<dp[i][j]<<" ";
// }
// cout<<endl;
// }
cout<<dp[n]<<endl;
return 0;
}
416. 分割等和子集
思路
本题可抽象为一个背包问题:从数组nums中选取物品,放入容量为sum/2的背包中,看背包能否**装满,**装满就意味着从数组中选取元素可以填充为数组和的一半,代表可以分割成等和子集
还需注意,本题需要判断是否装满的逻辑,实现方式为:把物品的重量和价值设为相同值,即令nums中元素既为其重量也为其价值:
如果填满了,则总 重量=背包容量=target ,总价值=dp[target] = 总重量,推导出 dp[target] = target;
如果填不满,则target≠dp[target]
剩余思路和01背包 一维问题完全一样,最终返回dp[target] == target即可
cpp
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(auto i :nums) sum+=i;
if(sum%2 !=0) return false;
int target = sum/2;
//dp[j]含义:j容量的背包,所有物品任选放入,得到的最大价值
vector<int> dp(target+1,0);
//初始化dp数组:直接初始化为0,这样就满足更新条件
for(int i=0; i<nums.size(); ++i){
for(int j=target; j>=nums[i]; --j){
dp[j] = max(dp[j], dp[j-nums[i]]+nums[i]);
}
}
return dp[target] == target;
}
};