0-1背包问题
给定n个重量为w1/w2/w3.../wn
,价值为v1/v2/v3.../vn
的物品,容量为C
的背包,求这个背包可以装下的价值最高的子集,每个物品只能使用一次
w = {2,1,3,2} //重量
v = {12,10,20,15} // 价值
C = 5 // 容量
#最佳子集为 [2,1,2] 12+10+15 = 37
暴力递归
对每个物品,都有选择/不选
两个状态,这样解空间就可以描述成一个状态树,
可以很像求子集
的问题,不同的地方就是,子集的重量和需要小于背包重量,否则需要剪枝
,如2->1->3
不满足报容量,则3
不可成为2->1
这个子路径的叶节点,然后统计每一个子集的价值总和,比较得出最大值即可;
void backtrack(vector<int> w,vector<int> v,int C,int index,int& res,int val){
if(index == w.size() || C<=0){
res = max(res,val);
return ;
}
if(C >= w[index]){
backtrack(w,v,C-w[index],index+1,res,val+v[index]);
}
backtrack(w,v,C,index+1,res,val);
}
int main() {
vector<int> w = {2,1,3,2};
vector<int> v = {12,10,20,15};
int res = 0,C=5;
backtrack(w,v,C,0,res,0);
cout<<res<<endl;
return 0;
}
带备忘录的递归
因为上述方法会存在很多重复子问题的计算,所以我们可以保存子问题计算的结果,从而达到剪枝的效果
int backtrack2(vector<int> w,vector<int> v,int C,int index,int val,vector<vector<int>>& memo){
if(index == w.size() || C <= 0){
return val;
}
if(memo[index][C]!=-1)
return memo[index][C];
int aa = 0;
if(C >= w[index]){
aa = backtrack2(w,v,C-w[index],index+1,val+v[index],memo);
}
aa = max(aa,backtrack2(w,v,C,index+1,val,memo));
memo[index][C] = aa;
return aa;
}
int main() {
vector<int> w = {2,1,3,2};
vector<int> v = {12,10,20,15};
int res = 0;
vector<vector<int>> memo(w.size(),vector<int>(6,-1));
res = backtrack2(w,v,5,0,0,memo);
cout<<res<<endl;
return 0;
}
动态规划
我们可以使用一个二维数组dp[n][C+1]
,表示第几个物体 在C容量下的价值;
则dp[i][j] = max(dp[i-1][j] , dp[i-1][j-w[i] ] + v[i])
表示w[i]
第i
个物体不装入背包的话,则直接由上一层价值传递,如果w[i]
装入背包的话,则由dp[i-1][j-w[i]]
上一层 减去物体重量时的价值 + 物体本身的价值,取两者大,表格示意图如下:
| 背包容量
----------
物体id |
int dfs_dp(vector<int> w ,vector<int> v,int C){
vector<vector<int>> dp(w.size(),vector<int>(C+1,0));
for (int i = 0; i <= C; i++) {
dp[0][i] = w[0] <= i ? v[0] : 0;
}
for(int i=1;i<w.size();i++){
for(int j=1;j <= C;j++){
dp[i][j] = dp[i-1][j];
if( j >= w[i] ){
dp[i][j] = max(dp[i][j],dp[i-1][j-w[i]]+v[i]);
}
}
}
for(auto i:dp){
for(auto j:i)
cout<<j<<" ";
cout<<endl;
}
return dp[w.size()-1][C];
}
int main() {
vector<int> w = {2,1,3,2};
vector<int> v = {12,10,20,15};
int res = 0;
res = dfs_dp(w,v,5);
cout<<res<<endl;
return 0;
}
/*
0 0 12 12 12 12
0 10 12 22 22 22
0 10 12 22 30 32
0 10 15 25 30 37
*/
状态空间压缩
leetcode 416 Partition Equal Subset Sum
给定一个仅包含正整数的非空数组,确定该数组是否可以分成两部分,要求两部分的和相等
[1,2,3,6] --> [1,2,3],[6]
int dfs_dp(vector<int> w ,int C){
// 抽象为: 使用n个(重量w)的物体,能否正好装满C容量的背包;
// dp[n][c] 表示 使用n个物体,是否可以装满 C容量的背包;
int n = w.size();
vector<vector<bool>> dp(n,vector<bool>(C+1,false));
dp[0][w[0]] = 1; // 初始,使用w[0]物体,可以正好装满w[0]容量的背包
// dp[i][j] = dp[i-1][j]; 当前物体不入袋,继承上一个物体的状态
// dp[i][j] = dp[i][j] || dp[i-1][j-w[i]]; 当前物体入袋||不入袋,只要有一个状态可以装满 即为True
for(int i=1;i<n;i++){
for(int j=0;j<=C;j++){
dp[i][j] = dp[i-1][j];
if(j>=w[i])
dp[i][j] = dp[i][j] || dp[i-1][j-w[i]];
}
}
for(auto i:dp){
for(auto j:i)
cout<<j<<" ";
cout<<endl;
}
return dp[n-1][C];
}
int main() {
vector<int> v1 = {1,2,3,6};
bool flag = dfs_dp(v1,6);
cout<<flag<<endl;
return 0;
}
/*
0 1 0 0 0 0 0
0 1 0 1 0 0 0
0 1 0 1 1 0 1
0 1 0 1 1 0 1
*/