说说几种背包问题(java实现)



背包问题是经典的动态规划问题。分成以下几种细说一下背包问题。代码为java实现。


1. 01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

定义一个二阶矩阵dp[N+1][V+1],
dp[i][j]表示在 只能选择前i个物品,背包容量为j的情况下,背包中物品的最大价值
这里之所以要N+1和V+1,是因为第0行表示只能选择第0个物品的时候,即没有物品的时候第0列表示背包的体积为0的时候,即不能装任何东西的时候.

已知dp[i-1][j],要求dp[i][j],则需要状态转移方程
对于dp[i][j]有两种情况:
  1.不选择当前的第i件物品/第i件物品比背包容量要大
            dp[i][j] = dp[i-1][j]
  2.选择当前的第i件物品(潜在要求第i件物品体积小于等于背包总容量),则能装入的物品最大价值为:当前物品的价值 加上 背包剩余容量在只能选前i-1件物品的情况下的最大价值。表示为:
          dp[i][j] = dp[i-1][j-v[i]] + w[i]
dp[i][j]在两种情况中选择比较大的情况作为当前的最优解;
即:
   if(j >= v[i]):
      dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])
   else:
      dp[i][j] = dp[i-1][j]

以上的动态规划需要一个二维的dp空间。我们可以优化为一个一维的dp数组。此时,dp[v]就表示在v大小的背包空间下,可以放入的最大的物品价值 。此时的物品依次放入。当我们已知第i-1个物品放入后,v的最大值,那么加入第i个物品时,最大值要么是放入第i个物品,要么是原来的值。表达式:
      dp[v] = Math.max( dp[v - v[i] ) + w[i] , dp[v])
参考代码如下:

import java.util.Scanner;

public class Main{
    public static void main(String[] args) throws Exception {
        // 读入数据的代码
        Scanner reader = new Scanner(System.in);
        // 物品的数量为N
        int N = reader.nextInt();
        // 背包的容量为V
        int V = reader.nextInt();
        // 一个长度为N的数组,第i个元素表示第i个物品的体积;
        int[] v = new int[N + 1] ;
        // 一个长度为N的数组,第i个元素表示第i个物品的价值;
        int[] w = new int[N + 1] ;

        for (int i=1 ; i <= N ; i++){
        //读入是第一个数索引为1
        // 接下来有 N 行,每行有两个整数:v[i],w[i],用空格隔开,分别表示第i件物品的体积和价值
            v[i] = reader.nextInt();
            w[i] = reader.nextInt();
        }
        reader.close() ;

        // 二维dp

        int[][] dp = new int[N+1][V+1];
        dp[0][0] = 0;
        //此处i从1开始是因为上面v[i]和w[i]是从1开始存的
        for(int i = 1; i <= N; i++){
            for(int j = 0; j <= V; j++){
                if(j >= v[i]){
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
       //压缩为一维dp数组
        int[] dp = new int[V+1];
        dp[0] = 0;
        for(int i = 1; i <= N; i++){
            for(int j = V; j >= v[i]; j--){
                dp[j] = Math.max(dp[j], dp[j-v[i]] + w[i]);
            }
        }

2.完全背包问题

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
可以模仿01背包,dp[i][j]记录前i个物品放入j空间的最大值。此时,由于一个物品可是使用无限次数,dp[i][j]应该从dp[i][j-v[i]]来而不是dp[i-1][j-v[i]]来。表达式为


dp[i][j]=Math.max(dp[i][j-v[i]]+w[i],dp[i-1][j]) 同样,完全背包问题一样可以优化至一维dp空间。 01背包和完全背包使最常用的背包问题。很多其他背包问题可以转换为这两个问题来思考。 参考代码如下:

//数据读取和上一题01背包相同
    //模仿01背包,多维的dp。dp[i][j]记录前i个物品放入j空间的最大值。
        int[][] dp=new int[N+1][V+1];
        for(int i=1;i<=N;i++){
            for(int j=0;j<=V;j++){
                if(j>=v[i]){
                    //此时的状态方程,比较的大小
                    //注意i-1和i的状态区别
                    dp[i][j]=Math.max(dp[i][j-v[i]]+w[i],dp[i-1][j]);
                }else{
                    dp[i][j]=dp[i-1][j];
                }
               
            }
        }
        System.out.println(dp[N][V]);

    //初步优化为一维数组
    //dp[v]表示在v空间下装入的最大价值
        int[] dp=new int[V+1];
        for(int i=1;i<=N;i++){
            for(int j = V; j >= v[i]; j --){
            	//第i个物品可以使用0-k个,和不使用第i个物品的最大值比较,找到较大的一个
                //逐一找最大值,增加循环,用变量k遍历计算最大值。
                for(int k = 0; j-k* v[i] >= 0; k ++){
                    dp[j] = Math.max(dp[j] , dp[j - k * v[i]] + k * w[i]);
                }
            }
        }
        System.out.println(dp[V]);  
    
    //再优化。
        int[] dp=new int[V+1];
        for(int i=1;i<=N;i++){
        //和01背包相比在于01背包从后向前遍历,由于使用到之前的状态,从后向前时前面的状态为0,确保了一个物品只使用了一次。
        //完全背包使用从前向后遍历,前面的状态先遍历。此时后面的状态再计算时,使第i个物品重复使用。
            for(int j =  v[i]; j <= V;  j ++){
                dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
            }
        }
        System.out.println(dp[V]);

3 多重背包问题

有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
思路:
  参考完全背包问题,就是将完全背包中的数量限制改为si. (体现在for循环中)
优化:
  利用二进制,将背包数量分解,变为01背包问题。(他人处学习来的思路和代码)。
参考代码:

//直接做法。
        int[] dp = new int[V+1];
        for(int i = 1; i <= N; i ++){
            for(int j = V; j >= v[i]; j --){
            //此处加入一个限制条件k<=s[i]即可
            //k可以从1开始,因为0就是dp[j]
                for(int k = 1; j - k * v[i] >= 0 && k <= s[i]; k ++ ){
                    dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
                }
            }
        }
        System.out.println(dp[V]);       
        
        //参考地址https://www.acwing.com/solution/content/7495/
        //优化的多重背包问题。        
        //将数量s分解。例如有一个体积为v,值为w,数量为7的。则分解为[v,w],[2v,2w],[4v,4w]三个物体            
        //将所有物体数量都分解。则化解为了01背包问题。       
        int maxN = 200002;
        int[] v = new int[maxN];
        int[] w = new int[maxN];
        Scanner jin = new Scanner (System.in);
        void run(){
            int n = jin.nextInt();
            int m = jin.nextInt();
            int p = 1;
            for (int i = 1; i <= n ; i++){
                int V = jin.nextInt();
                int W = jin.nextInt();
                int S = jin.nextInt();
                int k = 1;
                while (S > k){
                    v[p] = V*k;
                    w[p] = W*k;
                    S -= k;
                    k *= 2;
                    p++;
                }
                if (S > 0){
                    v[p] = V*S;
                    w[p] = W*S;
                    p ++;
                }
            }
            //到此为止,原来所有的物体分解为共p个独立的物体,每个限制使用1次,化解为01背包问题。       
            //共有p个物体,放入总体积m的背包。体积为v,价值为w
            int res = dp(p, m);
            System.out.println(res);
        }
        //同01背包问题。
        int dp(int n, int m){
            int[] f = new int[maxN];
            for (int i= 1; i <= n ; i ++){
                for (int j = m ; j>= v[i] ; j--){
                    f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
                }
            }
            return f[m];
        }
        public static void main(String[] args) {new solution().run();}
        
        //利用优先队列优化多重背包问题。看到过大佬用过这个思路。
        //(待学习)

4.混合背包问题

有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。
si=−1 表示第 i 种物品只能用1次;
si=0 表示第 i 种物品可以用无限次;
si>0 表示第 i 种物品可以使用 si 次;
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

思路:
分类讨论。01背包可以看作只使用一次的多重背包问题。 参考代码:

public static void main(String[] args){
            Scanner sc = new Scanner(System.in);
            int N = sc.nextInt(); // 物品个数
            int V = sc.nextInt(); // 背包总容量
            int[] dp = new int[V + 1];
            for(int i = 0; i < N; i++){
                int v = sc.nextInt(); // 体积
                int w = sc.nextInt(); // 价值
                int s = sc.nextInt(); // 数量
                if(s == 0){
                    // 完全背包问题
                    for(int j = v; j <= V; j++){
                        dp[j] = Math.max(dp[j], dp[j - v] + w);
                    }
                }else{
                    // 多重背包问题,01背包是多重背包的特例,可以一并处理
                    s = Math.abs(s);
                    for(int j = 1; s >= j; s -= j, j *= 2){
                        for(int k = V; k >= j * v; k--){
                            dp[k] = Math.max(dp[k], dp[k - j * v] + j * w);
                        }
                    }
                    if(s > 0){
                        for(int j = V; j >= s * v; j--){
                            dp[j] = Math.max(dp[j], dp[j - s * v] + s * w);
                        }
                    }
                }
            }
            System.out.println(dp[V]);
        }

5.二维背包问题

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
初始的01背包问题多加入了一个限制 求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
思路: 与01背包一样,二维dp扩充为三维dp

public int two_dimension_knapsack_problem_1(int N, int V, int M, int[] v, int[] m, int[] w){
            int[][][] dp = new int[N+1][V+1][M+1];
            for(int i = 1; i <= N; i++){
                for(int j = 1; j <= V; j++){
                    for(int k = 1; k <= M; k++){
                        if(j < v[i] || k < m[i]){
                            // 客观条件限制,不能选择当前物品N
                            dp[i][j][k] = dp[i-1][j][k];
                        }else {
                        //基本相同,多个限制
                            dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-v[i]][k-m[i]] + w[i]);
                        }
                    }
                }
            }
            return dp[N][V][M];

注:许多代码与思路来源于anwin网站与相关题目题解。