目录
(一)背景
(二)01背包
基础做法
优化
(三)完全背包
基础做法
优化
(四)01背包和完全背包问题总结
(五)多重背包
基础解法
优化思路1
(六)混合背包
(七)二维费用的背包问题
(八)分组背包
(九) 背包问题求解具体选择方案
思路
定义状态转移方程2
求解具体方案思路
思路来源于一个B站up主,大雪菜。然后他还搞了个网站AcWing,支持在线刷题。很强就是了。根据大神讲的方法,我这就简单总结整理了一下。
状态转移方程:定义f[i][j]:前i个物品,背包容量j下的最优解
1)当前背包容量不够(j < w[i]),为前i-1个物品:f[i][j] = f[i-1][j]
2)当前背包容量够,判断选与不选第i个物品
选:f[i][j] = f[i-1][j-w[i]] + v[i] 不选:f[i][j] = f[i-1][j]
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
int[][] res=new int[N+1][V+1];
for(int i=1;i<=N;i++){
int v=sc.nextInt();
int w=sc.nextInt();
for(int j=1;j<=V;j++){
if(j
思路:
1.二维数组降低为一维:利用上层循环在本层循环还未被更新过的特性,到等效替代。故f[i][j]=f[i-1][j]可以等效为f[j]=f[j]; 等号右边的f[j]即为还未被更新的值,即i-1层的值。
2.内存循环从大到小遍历:因为要求f[i-1][j-v],其要表示成f[j-v]的话,则f[j-v]要表示是i-1层的值,由于j-v比j小,如果j从小到大循环,则赋值给f[j]的时候f[j-v]已经被更新为了第i层的值,与需求不符。故优化成从大到小遍历,保证了赋值f[j]的时候f[j-v]还未被更新,为i-1层的值。
循环部分优化结果:
for(int i=1;i<=N;i++){
int v=sc.nextInt();
int w=sc.nextInt();
for(int j=V;j>=v;j--){
res[j]=Math.max(res[j],res[j-v]+w);
}
}
最基础得写法是个三层循环,因为在选择第i件物品时,可以选择多件,假设1~k,则容量足够时:
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 , .....)
用j-v替代j,则公式等效为:
f[i,j-v]=max( f[i-1,j-v], f[i-1,j-2*v] + w , f[i-1,j-2*v]+2*w , .....)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
通过公式等效换算可以替代为:res[i][j]=Math.max(res[i-1][j],res[i][j-v[i]]+w[i]);
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner in=new Scanner(System.in);
while(in.hasNext()){
int N=in.nextInt();
int V=in.nextInt();
int[][] res=new int[N+1][V+1];
for(int i=1;i<=N;i++){
int v=in.nextInt();
int w=in.nextInt();
for(int j=1;j<=V;j++){
if(v>j) res[i][j]=res[i-1][j];
else
res[i][j]=Math.max(res[i-1][j],res[i][j-v]+w);
}
}
System.out.println(res[N][V]);
}
}
}
主要是对两层循环的地方进行优化,思路和01背包一样,即减少矩阵维度。
降为1维:因为res[i][j]=Math.max(res[i-1][j],res[i][j-v[i]]+w[i]); 用的就是第i层的数据,且故应该是当前更新的数据,即j遍历仍使用从小到大的顺序,保证了值是更新后的。
for(int i=1;i<=N;i++){
int v=in.nextInt();
int w=in.nextInt();
for(int j=1;j<=V;j++){
if(v>j) res[j]=res[j];
else
res[j]=Math.max(res[j],res[j-v]+w);
}
}
代码逻辑精简:
当v[i]>j时,不用再赋值仍未以前得值不变,再次赋值操作就可以省的,原值不变,不做任何处理。故取最大值的时候,此时条件一定是V[i]>=j。
int [] res= new int[V+1];
for(int i=1;i<=N;i++){
int v=in.nextInt();
int w=in.nextInt();
for(int j=v;j<=V;j++){
res[j]=Math.max(res[j],res[j-v]+w);
}
}
仔细比较优化后的代码,区别仅在于第二层循不同,前者是逆序遍历、后者是顺序遍历。主要原因就是由于使用的是第i-1层的数据还是i层的。
1.01背包:f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
2.完全背包:f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
多加一层循环,遍历k可可能的取值范围,本质上还是01背包问题
f[i][j] = max(f[i - 1][j], f[i -1][j -v] +w, f[i-1][j-2v]+2w,...…)
其他代码和之前一致,循环部分修改了:
for(int i=1;i<=N;i++){
int v=sc.nextInt();
int w=sc.nextInt();
int s=sc.nextInt();
for(int j=V;j>=v;j--){
for(int k=1;k<=s;k++){
if(j>=k*v)
res[j]=Math.max(res[j],res[j-k*v]+k*w);
}
}
}
二进制展开--将重复的物品展开,视为更多件的01背包问题
分析: 例如7=1+2+4;故7件重复的A物品(v,w)选择可以视为:
有1件A(v,w)、2A(2v,2w)、4A(4v,4w)这三件物品去做选择做01背包问题。故原方案有7件重复物品需要进行7次循环,现在范围缩小成了3件物品。
对于不是2的整数幂的件数,例如13,可转换为1+2+4+6,转换为对应的这4件物品(A\2A\4A\6A)的01背包问题,即优化后的件数num=log(N*K)+1;
时间复杂度由原来的O(N*V*K)优化为了O(num*V);
直观来看,若N=1000,V=1000,K=1000,原时间复杂度即约为2^30;优化后为20*1000=2^14左右吧。
import java.util.*;
public class Main{
static class Goods{
int v,w;
public Goods(int v,int w){
this.v=v;
this.w=w;
}
}
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
ArrayList goods=new ArrayList<>();
//二进制展开物品件数
for(int i=0;i0)
goods.add(new Goods(s*v,s*w));
}
//01背包求解
int[] res=new int[V+1];
for(int i=0;i=goods.get(i).v;j--){
res[j]=Math.max(res[j],res[j-goods.get(i).v]+goods.get(i).w);
}
}
System.out.println(res[V]);
}
}
思路:这题包括了3种情况:01背包、完全背包、多重背包。
由之前多重背包问题可知,这类问题可以转换为基础的01背包问题进行求解,所以这里实际上就只存在2种情况:01背包和完全背包。套用之前的代码模板进行分类讨论即可。
对于多重背包问题采取二进制位展开的优化思路将其转换为01背包问题。
import java.util.*;
public class Main{
static class Goods{
int v,w;
boolean flag;//true 代表01背包问题、false代表完全背包问题
public Goods(int v,int w,boolean f){
this.v=v;
this.w=w;
this.flag=f;
}
}
public static void main(String []args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
ArrayList goods=new ArrayList<>();
for(int i=0;i0
for(int m=1;m0) goods.add(new Goods(s*v,s*w,true));
}
}
int[] res=new int[V+1];
for(int i=0;i=goods.get(i).v;j--)
res[j]=Math.max(res[j],res[j-goods.get(i).v]+goods.get(i).w);
else
for(int j=goods.get(i).v;j<=V;j++)
res[j]=Math.max(res[j],res[j-goods.get(i).v]+goods.get(i).w);
}
System.out.println(res[V]);
}
}
根据题意:多加了一个约束条件,本质上仍未01背包问题,套用01背包模板直接再嵌套一层循环即可。
import java.util.*;
public class Main{
public static void main(String []args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
int M=sc.nextInt();
int[][] res=new int[V+1][M+1];
for(int i=1;i<=N;i++){
int v=sc.nextInt();
int m=sc.nextInt();
int w=sc.nextInt();
for(int j=V;j>=v;j--)
for(int k=M;k>=m;k--)
res[j][k]=Math.max(res[j][k],res[j-v][k-m]+w);
}
System.out.println(res[V][M]);
}
}
思路:
i组物品、该组可以选也可以不选,总体层面上看仍是01背包问题;即求每组选与不选的最大值;
每一组有k个物品,每组物品最多选1次,故遍历这k个物品,求出最大值即可。
定义状态表达式:
f(i,j)表示从前i组选,体积小于等于j的选法
不选第i组:f(i,j)=f(i-1,j);
选第i组(即从k件物品中进行遍历比较):f(i,j)=max{f(i-1,j-v[k])+w[k]};
优化为一维的(j循环由大到小):f(j)=max{f(j-v[k])+w[k]};
import java.util.*;
public class Main{
public static void main(String []args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
int[] res=new int[V+1];
for(int i=1;i<=N;i++){
//获取每组的物品参数
int K=sc.nextInt();
int[] v=new int[K];
int[] w=new int[K];
for(int c=0;c=0;j--)
for(int k=0;k
注意:在最后一层k的for循环遍历中,有一个if(v[k]<=j)的判断,即判断第i组的第k件物品容量是否小于当前背包可容纳量j。若可以容纳,则考虑放入第k件物品,比较取最大值。
最开始我想当然的把这个if(v[k]<=j)判断加入到了k遍历for循环的判断条件里,即for(int k=0;k 而普通01背包中第二层j循环里j>=v加入到for循环条件里,若j f[i][j]表示从第i个物品开始,一直选到最后一个物品,保持不超过容量,价值最大的最优解。 不选第i件物品:f[i][j]=f[i+1][j]; ——最优解等同于从第i+1个物品到最后一个元素总容量为j的最优解; 选第i件物品:f[i][j]=f[i+1][j-v[i]]+w[i]; ——最优解等于当前物品的价值w[i]加上从第i+1个物品到最后一个元素总容量为j−v[i]的最优解。 所以这里i应该从后往前进行遍历(注意:首先得先遍历获得v[i],w[i]数组,保证其输入顺序,如果在第一层循环里再进行赋值,因为i是由大到小遍历,故此时的v,w顺序就反了) f[i][j]=max{f[i+1][j] , f[i+1][j-v[i]]+w[i]}; 同样的,通过这种状态方程也能解决01背包问题,求背包问题最优解代码如下,问题的最终结果最优解的答案应该为f[0][V],即从第一件物品开始一直选到最后一个物品,不超过容量V,价值最大。 根据以上思路f[0][V]中存的就是最大价值,那么如何确定第i件物品在最优解中是不是必须选中呢?即比较从第i件物品开始选择一直到最后 和 从i+1件物品开始选择,一直到最后的最优解是否一致。 例如: f(1,V)=f(2,V-v[1])+w[1]:表示选择第2件物品能得到最优解(这里的1是索引,实际是第二个元素)。 f(1,V)=f(2,V):表示从第2件开始和从第3件开始选最优解一样,故不选第2件物品可得到最优解。 综上,判断第i+1件物品是否必须选择才能得到最优解,即判断f(i,j)=f(i+1,j-v[i])+w[i]是否成立即可。(此时背包容量j需要大于v[i],否则装不下当前物品,此物品肯定不选,不必进入条件判断。) 注意的是,在从小到大遍历i时找到最优解的必须物品i时,需要将当前容量也相应减去v[i](找到了,背包容量相应减少),继续后续物品的遍历。 完整代码如下: (九) 背包问题求解具体选择方案
思路
定义状态转移方程2
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
int[] v=new int[N];
int[] w=new int[N];
for(int i=0;i
求解具体方案思路
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int N=sc.nextInt();
int V=sc.nextInt();
int[] v=new int[N];
int[] w=new int[N];
//顺序存入物品属性值
for(int i=0;i