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


    背包问题总结分析

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

    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]);
        }
    }
    
  • 相关阅读:
    Java如何编写自动售票机程序
    install windows service
    redis SERVER INSTALL WINDOWS SERVICE
    上传文件
    This problem will occur when running in 64 bit mode with the 32 bit Oracle client components installed.
    解决Uploadify上传控件加载导致的GET 404 Not Found问题
    OracleServiceORCL服务不见了怎么办
    Access to the temp directory is denied. Identity 'NT AUTHORITYNETWORK SERVICE' under which XmlSerializer is running does not have sufficient permiss
    MSSQL Server 2008 数据库安装失败
    数据库数据导出成XML文件
  • 原文地址:https://www.cnblogs.com/buptleida/p/13411787.html
Copyright © 2020-2023  润新知