背包问题是很多中大厂笔试机考阶段的重要题目来源,之前很多次都吃瘪了,为了增强自己的能力,还是很有必要进行专项学习与复习的。
主要参考acwing中的背包问题进行一个记录与学习,其中会引用大佬们的题解并附上个人理解进行说明与记录。
01背包问题
是背包问题的鼻祖,很多问题都是在01背包的基础之上进行拓展实现的。
问题描述:
有 N 件物品和一个容量为 V 的背包,每件物品有各自的价值且只能被选择一次,要求在有限的背包容量下,装入的物品总价值最大。
变量定义:
f[i], N 件物品,背包容量j下的最优解。
状态转移方程:
f[j] = max(f[j], f[j - v[i]] + w[i])
二维状态表示:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
/**
作者:深蓝
链接:https://www.acwing.com/solution/content/1374/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
**/
一维优化,这个也是我们常用的:
注意:枚举背包容量j必须从m开始。
参考解释1:如果使用顺序,会先更新f[4],再更新f[7],对于这个书包问题来讲,就是有可能,在更新f[4]的时候,已经把这次能加的物品加进来了,然后更新f[7]的时候,还有可能再加一次,所以必须使用逆序,保证,f[4]是没有加入新物品前,背包里的最优解。
参考解释2:若j从小到大,f[j-v[i]]中,由于j-v[i]小于j,f[j-v[i]]已经在i这层循环被计算了,而我们想要的f[j-v[i]]应该是i-1层循环里面的,所以j从大到小的话保证此时的f[j-v[i]]还未被计算,也就是第i-1层的数据
C++版本:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int f[MAXN];
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++) {
// int v, w;
// cin >> v >> w; // 边输入边处理
for(int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
cout << f[m] << endl;
return 0;
}
/**
作者:深蓝
链接:https://www.acwing.com/solution/content/1374/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
**/
Java版本:
// Java版本的01背包
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int n,v;
int[] dp = new int[1000*1000+5];
while(in.hasNextInt()){
n = in.nextInt();
v = in.nextInt();
int[] vol = new int[n+1];
int[] weight = new int[n+1];
for(int i=1;i<=n;i++){
vol[i] = in.nextInt();
weight[i] = in.nextInt();
}
// 01背包问题,双重循环需要先遍历物品再遍历体积
for(int i=1;i<=n;i++)
for(int j = v; j >= vol[i]; j--){
dp[j] = Math.max(dp[j], dp[j - vol[i]] + weight[i] ) ;
}
System.out.println(dp[v]);
}
}
}
引用视频中的截图:
01背包的例题:
注意问题是要求背包装满,还是求背包最多能装多少
完全背包问题
问题描述:
有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
二维情况下的状态转移过程:
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
如果还不够真实,那么继续引用大佬的模拟过程:
首先dp数组初始化全为0:给定物品种类有4种,包最大体积为5,数据来源于题目的输入
v[1] = 1, w[1] = 2
v[2] = 2, w[2] = 4
v[3] = 3, w[3] = 4
v[4] = 4, w[4] = 5i = 1 时: j从v[1]到5
dp[1] = max(dp[1],dp[0]+w[1]) = w[1] = 2 (用了一件物品1)
dp[2] = max(dp[2],dp[1]+w[1]) = w[1] + w[1] = 4(用了两件物品1)
dp[3] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] = 6(用了三件物品1)
dp[4] = max(dp[4],dp[3]+w[1]) = w[1] + w[1] + w[1] + w[1] = 8(用了四件物品1)
dp[5] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] + w[1] + w[1] = 10(用了五件物品)i = 2 时:j从v[2]到5
dp[2] = max(dp[2],dp[0]+w[2]) = w[1] + w[1] = w[2] = 4(用了两件物品1或者一件物品2)
dp[3] = max(dp[3],dp[1]+w[2]) = 3 * w[1] = w[1] + w[2] = 6(用了三件物品1,或者一件物品1和一件物品2)
dp[4] = max(dp[4],dp[2]+w[2]) = 4 * w[1] = dp[2] + w[2] = 8(用了四件物品1或者,两件物品1和一件物品2或两件物品2)
dp[5] = max(dp[5],dp[3]+w[2]) = 5 * w[1] = dp[3] + w[2] = 10(用了五件物品1或者,三件物品1和一件物品2或一件物品1和两件物品2)i = 3时:j从v[3]到5
dp[3] = max(dp[3],dp[0]+w[3]) = dp[3] = 6 # 保持第二轮的状态
dp[4] = max(dp[4],dp[1]+w[3]) = dp[4] = 8 # 保持第二轮的状态
dp[5] = max(dp[5],dp[2]+w[3]) = dp[4] = 10 # 保持第二轮的状态i = 4时:j从v[4]到5
dp[4] = max(dp[4],dp[0]+w[4]) = dp[4] = 10 # 保持第三轮的状态
dp[5] = max(dp[5],dp[1]+w[4]) = dp[5] = 10 # 保持第三轮的状态上面模拟了完全背包的全部过程,也可以看出,最后一轮的dp[m]即为最终的返回结果。
作者:polaris
链接:https://www.acwing.com/solution/content/3986/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
C++代码实现:
#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i = 1 ; i <= n ;i ++)
{
cin>>v[i]>>w[i];
}
for(int i = 1 ; i<=n ;i++)
for(int j = v[i] ; j<=m ;j++)
{
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
}
/**
作者:Charles__
链接:https://www.acwing.com/solution/content/5345/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
**/
多重背包问题
问题描述:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi ,价值是 wi 。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
分析:
当 si=1 时,相当于01背包中的一件物品。
当 si>1 时,相当于01背包中的多个一件物品。
故我们可以将问题拆分(把多重背包拆成01背包)
#include <bits/stdc++.h>
using namespace std;
int a[10005],b[10005],t=0,n,m,dp[10005]={ },w,v,s;
int main()
{
cin>>n>>m;
while(n--)
{
cin>>v>>w>>s;
while(s--)
{
a[++t]=v;
b[t]=w;
}//死拆,把多重背包拆成01背包
}
for(int i=1;i<=t;i++)
for(int j=m;j>=a[i];j--)
dp[j]=max(dp[j-a[i]]+b[i],dp[j]);//直接套01背包的板子
cout<<dp[m]<<endl;
return 0;
}
/**
作者:9年级的蒟蒻
链接:https://www.acwing.com/solution/content/13873/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
**/
总结:
背包问题是一个动态规划的分支大类,有的时候临场按照动态规划的状态表示与状态计算可能并不能想清楚,比较高效的方式还是利用模板。
先遍历物品,然后再遍历容量即可。
注意容量是由大到小还是由小到大。
参考资料:
AcWing 2. 01背包问题(状态转移方程讲解) - AcWing