• 【数据结构与算法】背包问题总结梳理


    背包问题总结分析

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

    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]);
        }
    }
    
  • 相关阅读:
    spring 声明式事务的坑 @Transactional 注解
    这样学Linux基本命令,事半功倍
    NIO buffer 缓冲区 API
    Java技术——你真的了解String类的intern()方法吗
    Spring中配置数据源的4种形式
    Java集合框架List,Map,Set等全面介绍
    阿里面试题:关于类中静态,非静态,构造方法的执行顺序
    web.xml加载顺序
    MyBatis 延迟加载,一级缓存,二级缓存设置
    mybatis 调用存储过程
  • 原文地址:https://www.cnblogs.com/buptleida/p/13411787.html
Copyright © 2020-2023  润新知