讲的最透彻易懂的背包问题--思路来源于AcWing

目录

(一)背景

(二)01背包

基础做法

 优化

 (三)完全背包

基础做法

 优化

(四)01背包和完全背包问题总结

(五)多重背包

基础解法

 优化思路1

(六)混合背包

(七)二维费用的背包问题

(八)分组背包

(九) 背包问题求解具体选择方案

思路 

定义状态转移方程2

 求解具体方案思路


(一)背景

思路来源于一个B站up主,大雪菜。然后他还搞了个网站AcWing,支持在线刷题。很强就是了。根据大神讲的方法,我这就简单总结整理了一下。

讲的最透彻易懂的背包问题--思路来源于AcWing_第1张图片

(二)01背包

讲的最透彻易懂的背包问题--思路来源于AcWing_第2张图片

基础做法

状态转移方程:定义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);
            }
        }

 (三)完全背包

讲的最透彻易懂的背包问题--思路来源于AcWing_第3张图片

基础做法

最基础得写法是个三层循环,因为在选择第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);
                }
            }

(四)01背包和完全背包问题总结

仔细比较优化后的代码,区别仅在于第二层循不同,前者是逆序遍历、后者是顺序遍历。主要原因就是由于使用的是第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]);//完全背包问题

(五)多重背包

讲的最透彻易懂的背包问题--思路来源于AcWing_第4张图片

基础解法

多加一层循环,遍历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);
                }
            }
        }

 优化思路1

二进制展开--将重复的物品展开,视为更多件的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]);
    }
}

(六)混合背包

讲的最透彻易懂的背包问题--思路来源于AcWing_第5张图片

思路:这题包括了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]);
    }
}

(七)二维费用的背包问题

讲的最透彻易懂的背包问题--思路来源于AcWing_第6张图片

根据题意:多加了一个约束条件,本质上仍未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]);
    }
}

(八)分组背包

讲的最透彻易懂的背包问题--思路来源于AcWing_第7张图片讲的最透彻易懂的背包问题--思路来源于AcWing_第8张图片

思路:

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=v部分进行计算赋值即可。

讲的最透彻易懂的背包问题--思路来源于AcWing_第9张图片

(九) 背包问题求解具体选择方案

讲的最透彻易懂的背包问题--思路来源于AcWing_第10张图片讲的最透彻易懂的背包问题--思路来源于AcWing_第11张图片

思路 

定义状态转移方程2

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,价值最大。

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=0;i--){
            for(int j=0;j<=V;j++){
                if(j

 求解具体方案思路

根据以上思路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](找到了,背包容量相应减少),继续后续物品的遍历。

完整代码如下:

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=0;i--){
            for(int j=0;j<=V;j++){
                if(j=v[i]&&res[i][curV]==res[i+1][curV-v[i]]+w[i]){
                int index=i+1;
                System.out.printf(index+" ");
                curV-=v[i];
            }
        }
    }
}

 

你可能感兴趣的:(Java)