背包问题总结梳理

背包问题总结分析

背包问题是个很经典的动态规划问题,本博客对背包问题及其常见变种的解法和思路进行总结分析

01背包

问题介绍

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 v[i],价值是 w[i]。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

基本思路

定义int[][] dpdp[i][j] 表示当容量为j时,对于前i个物品而言的最优放置策略(即最大价值)。对于物品 i 而言,只有放与不放,这两种选择。因此可以得到 状态转移方程

  • 放物品 i :dp[i][j] = dp[i - 1][j - v[i]] + w[i]

  • 不放物品 i :dp[i][j] = dp[i - 1][j]

直观方法:


// v和w数组长度都是 N + 1,v[0]和w[0]都是0

private static void backpack1(int N, int V, int[] v, int[] w) {



    int[][] dp = new int[N + 1][V + 1];

    for (int i = 1; i <= N; ++i) {

        for (int j = 1; j <= V; ++j) {

            dp[i][j] = dp[i - 1][j];

            if (j >= v[i]) {

                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);

            }

        }

    }

    System.out.println(dp[N][V]);

}

这种方法空间不是最优的。观察代码发现,dp[i]只跟dp[i-1]有关,所以可以将二维降成一维。

优化方法:


private static void backpack2(int N, int V, int[] v, int[] w) {

    int[] dp = new int[V + 1];

    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]);

        }

    }

    System.out.println(dp[V]);

}

注意:

内层循环不能顺序枚举。dp[j - v[i]] 实际上相当于 dp[i - 1][j - v[i]] ,而不是dp[i][j - v[i]] ,如果顺序枚举, dp[i] 的 j - v[i] 的位置已经被计算过,覆盖了。所以应该通过倒序枚举来规避这个问题。

两个要点:

  • 若 dp[] 全部初始化为0,计算结果的 dp[V] 就是答案;

  • 若 dp[0] 初始化为0,其它元素全部初始化为负无穷,则最后遍历dp[]得到最大值为答案。

解释如下:

dp[V] 一定是最大值。同样遍历了所有物品情况下,容量 V 大于 V - X ,最后得到的价值 dp[V] 必然大于 dp[V - X]。

dp数组初始化值全为 0 ,则允许dp[V]从任何一个初始项转化而来,并不一定是 dp[0]。最终结果如果从 dp[k] 转化而来,说明有 k 体积的空余。但是,如果我们更改一下dp数组初始化的情况:

将 dp[0][0] 取0 ,dp[0][1] ~ dp[0][V]全部取负无穷,同样计算,得到的结果 dp[N][1] ~ dp[N][V] 中最后一位数不一定是最大值。循环求MAX,可排除掉从“负无穷”初始值转化而来的结果。假设得到的结果 dp[N][Y] ,则该值为体积总和恰好等于 Y 的最大价值。

完全背包

问题介绍

与01背包的区别:所有物品可以无限件使用。其它都一样。

基本思路

跟01背包一样,一定需要一个for (int i = 1; i <= N; ++i)外层循环,枚举每个物品。内部循环相较于01背包需要发生呢个变化。需要枚举 v[i]~V 容量下,放置 1~k 个物品i,最大价值的情况,并记录进 dp 数组。因此直观思路是再套两层循环,如下所示。


for (int j = V; j >= v[i]; --j) {

	for(int k=1;k*v[i]<=j;++k){

    	dp[j] = Math.max(dp[j], dp[j - k * v[i]] + w[i]);

    }

}

实际上, k 的那一层循环是可以省略的。如下所示

完全背包解法:


private static void completeBackpack(int N, int V, int[] v, int[] w) {

    int[] dp = new int[V + 1];

    for (int i = 1; i <= N; ++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]);

}

如上述代码所示,内层遍历 j 采用正向枚举即可节省一层循环。前文提到过,在01背包里,这样枚举是错误的,因为dp[i][] 会把 dp[i-1][] 覆盖掉。但在本问题中可以巧妙利用其“覆盖”的特性,缩减时间复杂度。覆盖的过程,实际上就是原有的 dp 值加一个 w[i] 。对于每一个 dp[j] 而言,需要考虑是在 dp[j - v[i]] 加一个物品 i 的价值,还是不加物品 i 继续沿用 dp[j] 。for (int j = v[i]; j <= V; ++j)这样循环,最多可以加 (V - v[i] + 1)次物品,由于物品 i 体积大于等于 1,所以物品 i 的添加次数不可能超过 (V - v[i])/ 1 次,所以一定会遇到最优的情况。

涉及顺序的完全背包问题

即放入背包中的物品,顺序不同的序列被视为不同的组合,求满足target的总组合数。
例题:单词拆分组合总和IV

思路

将前面完全背包问题解决方案中两层循环倒过来即可解决该问题,即把对容量的遍历放在外层,物品的循环放在内层。前文的循环方式相当于去除了重复的组合。
换种思路来理解:假设物品1~ n,对于每一个容量K而言(K<=target),要从前一步抵达K的位置,有1~ n种可能。假设某物品体积为v,对于容量(K-v)而言也同样是遍历过n个物品,所以应该在内层循环遍历n个物品,这样一定枚举了所有排列情况。

示例代码如下:

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target+1];
        dp[0] = 1;
        for(int j=1;j<=target;++j){
            for(int item : nums){
                if(j>=item) dp[j] += dp[j-item];
            }
        }
        return dp[target];
    }
}

多重背包

问题介绍

在完全背包基础上,对每个物品限定数量。

普通解法


import java.util.Scanner;

public class Main{

    public static void main(String[] args) throws Exception{

        Scanner reader = new Scanner(System.in);

        int N = reader.nextInt();

        int V = reader.nextInt();

        int[] dp = new int[V + 1];

        for(int i=1;i<=N;++i){

            int v = reader.nextInt();

            int w = reader.nextInt();

            int s = reader.nextInt();

            for(int j=V;j>=v;--j){

                for(int k=1;k<=s&&k*v<=j;++k){

                    dp[j] = Math.max(dp[j],dp[j-k*v]+k*w);

                }

            }

        }

        System.out.println(dp[V]);

    }

}

二进制优化方法

实际上,当s非常大时,将物品划分为s个物品,转化为01背包问题来计算,这样时间复杂度非常巨大。有一个技巧,可以简化该问题:对于任意一个数S,分成数量不同的若干个数,这些数选或不选可以拼成小于S的任意一个数。
如何划分这个S便是问题的关键。试想,对于一个数 7 它的二进制形式是 111 ,每一位上取 1 或者取 0 正好可以描述“选物品”或者“不选物品”两个行为,因此可以想到将 7 划分为 1 + 2 + 4。对于二进制位全为 1 的数,可以使用上述方法进行划分。如果不是这样的数,譬如说10,该如何划分呢?
实际上可以划分为 1 + 2 + 4 + 3。要证明此猜想,只需要证明7~10之间的数一定能通过1、2、4、3这四个数选或不选来得到即可。由于 1、2、4 一定能得到5、6、7,因此 +3 一定能得到 8、9、10,所以得证。
二进制优化方法的代码如下所示:

import java.util.Scanner;
import java.util.LinkedList;
import java.util.List;
public class Main{
    public static void main(String[] args) throws Exception {
        Scanner reader = new Scanner(System.in);
        int N = reader.nextInt();
        int V = reader.nextInt();
        List<Integer> vList = new LinkedList<>();
        List<Integer> wList = new LinkedList<>();
        int[] dp = new int[V + 1];
        for (int i = 0; i < N; ++i) {
            int v = reader.nextInt();
            int w = reader.nextInt();
            int s = reader.nextInt();
            for (int k = 1; k <= s; k *= 2) {
                vList.add(k * v);
                wList.add(k * w);
                s -= k;
            }
            if (s > 0) {
                vList.add(s * v);
                wList.add(s * w);
            }
        }
        for (int i = 0; i < vList.size(); ++i) {
            int v = vList.get(i);
            int w = wList.get(i);
            for (int j = V; j >= v; --j) {
                dp[j] = Math.max(dp[j], dp[j - v] + w);
            }
        }
        System.out.println(dp[V]);
    }
}

混合背包问题

描述:物品一共有三类,第一类物品只能用一次(01背包),第二类物品能用无限次(完全背包),第三类物品最多用s次(多重背包)

思路

将01背包、完全背包、二进制优化的多重背包三个算法都结合起来,遍历到每个物品的时候做一个判断即可。

  • 遍历每一行输入,即每一类物品;
  • 如果是物品只能选一次,按照01背包方法,更新dp数组(计算每一个容量下,选或不选的最大价值);
  • 如果物品可以选无数次,则按照完全背包方法,更新dp数组;
  • 如果给定 s ,则将s按二进制分解为log(s)份,也按照01背包来计算。

具体的题目描述可参考混合背包问题,代码如下:

import java.util.Scanner;
public class Main{
    public static void main(String[] args) throws Exception {
        Scanner reader = new Scanner(System.in);
        int N = reader.nextInt();
        int V = reader.nextInt();
        int[] dp = new int[V + 1];
        for(int i=0;i<N;++i){
            int v = reader.nextInt();
            int w = reader.nextInt();
            int s = reader.nextInt();
            if(s == -1){// 01背包
                dp_01(dp, V, v, w);
            }else if(s == 0){ // 完全背包
                for(int j=v;j<=V;++j){
                    dp[j] = Math.max(dp[j],dp[j-v]+w);
                }
            }else{ // 多重背包
                for(int k=1;k<=s;s-=k,k*=2){
                    dp_01(dp, V, k*v, k*w);
                }
                if(s>0) dp_01(dp, V, s*v, s*w);
            }
        }
        System.out.println(dp[V]);
    }
    private static void dp_01(int[] dp, int V, int v, int w){
        for(int j=V;j>=v;--j){
            dp[j] = Math.max(dp[j],dp[j-v]+w);
        }
    }
}

二维费用背包问题

每个物品有两个属性:体积和重量。在01背包的基础上,多加入了一个维度“重量”,即费用从一维扩展到二维。

思路

将dp数组设置为二维数组,分别代表体积和重量两个维度,跟01背包相比多了一层循环。代码如下:

import java.util.Scanner;
public class Main{
    public static void main(String[] args){
        Scanner reader = new Scanner(System.in);
        int N = reader.nextInt();//物品数量
        int V = reader.nextInt();//体积上限
        int M = reader.nextInt();//重量上限
        int[][] dp = new int[V+1][M+1];
        for(int i=0;i<N;++i){
            int v = reader.nextInt();//物品体积
            int m = reader.nextInt();//物品重量
            int w = reader.nextInt();//物品价值
            for(int j=V;j>=v;--j){
                for(int k=M;k>=m;--k){
                    dp[j][k] = Math.max(dp[j][k],dp[j-v][k-m]+w);
                }
            }
        }
        System.out.println(dp[V][M]);
    }
}

分组背包问题

输入物品有 N 个组,每一组中只能选择一个物品。

思路

依然是在01背包的基础上做改动。每次选择时,假设组内有S个物品,则有S+1种决策,遍历这些决策,选取价值最大的即可。代码如下所示:

import java.util.Scanner;
public class Main{
    public static void main(String[] args){
        Scanner reader = new Scanner(System.in);
        int N = reader.nextInt();
        int V = reader.nextInt();
        int[] dp=new int[V+1];
        for(int i=0;i<N;++i){
            int s = reader.nextInt();
            int[] v = new int[s];
            int[] w = new int[s];
            for(int k=0;k<s;++k){
                v[k] = reader.nextInt();
                w[k] = reader.nextInt();
            }
            for(int j=V;j>0;--j){
                for(int k=0;k<s;++k){
                    if(j>=v[k]) 
                        dp[j] = Math.max(dp[j],dp[j-v[k]]+w[k]);
                }
            }
        }
        System.out.println(dp[V]);
    }
}
posted @ 2020-07-31 19:35  数小钱钱的种花兔  阅读(542)  评论(0编辑  收藏  举报