• 动态规划总结


    背包问题一直是动态规划的热点,也是各大公司笔试的常客,所以掌握基本的背包解题思路是很重要的

    0-1 背包问题

    题目

    N 件物品和一个容量为 V 的背包。第i件物品的费用是 c[i],价值是 w[i]。求解将哪些物品装入背包可使价值总和最大。

    解题思路:

    这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

    用子问题定义状态:即 f[i][v] 表示考虑将 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。则其状态转移方程便是:

    f[i][v] = max(f[i - 1][v], 
                  f[i - 1][v - c[i]] + w[i])
    

    这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。

    “将前 i 件物品放入容量为 v 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只牵扯前 i-1 件物品的问题。

    • f[i - 1][v]

    如果不放第 i 件物品,那么问题就转化为“前 i-1 件物品放入容量为 v 的背包中”,价值为 f[i-1][v]

    • f[i - 1][v - c[i]] + w[i]

    如果放第 i 件物品,那么问题就转化为“前 i-1 件物品放入剩下的容量为 v-c[i] 的背包中”,此时能获得的最大价值就是 f[i-1][v-c[i]] 再加上通过放入第i件物品获得的价值 w[i]

    代码实现

    public class Solution {
        public static void main(String[] args) {
            int[] w = {6, 29, 39};
            int[] v = {6, 10, 12};
            int W = 6;
            //6
            System.out.println(new Solution().knapsack01(w, v, W));
        }
    
        public int knapsack01(int[] w, int[] v, int W) {
            int len = w.length;
            //dp[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值
            //状态转移方程为:f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]+w[i]}
            int[][] dp = new int[len + 1][W + 1];
            //初始化:重量为0或者背包容量为0时最大价值为0,数组本来就是初始化值为0,跳过
    
            for (int i = 1; i <= len; i++) {
                for (int j = 1; j <= W; j++) {
                    //f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]+w[i]}
                    //当前最大价值等于放上一件的最大价值
                    dp[i][j] = dp[i - 1][j];
                    //如果当前物品(i-1)能放入背包
                    if (j >= w[i - 1]) {
                        //就考虑放入还是不放入↓
                        //这里需要注意,i - 1表示的是当前件,因为这里的i从1开始
                        dp[i][j] = Math.max(dp[i - 1][j], v[i - 1] + dp[i - 1][j - w[i - 1]]);
                    }
                }
            }
    
            return dp[len][W];
        }
    }
    

    优化空间复杂度

    以上方法的时间和空间复杂度均为 O(VN),其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到O。(如果需要)

    先考虑上面讲的基本思路如何实现,肯定是有一个主循环 i=1..N,每次算出来二维数组 f[i][0..V] 的所有值。那么,如果只用一个数组 f[0..V],能不能保证第i次循环结束后 f[v] 中表示的就是我们定义的状态 f[i][v] 呢?f[i][v] 是由 f[i-1][v]f[i-1][v-c[i]] 两个子问题递推而来,能否保证在推 f[i][v] 时(也即在第 i 次主循环中推 f[v] 时)能够得到 f[i-1][v]f[i-1][v-c[i]] 的值呢?事实上,这要求在每次主循环中我们以 v=V..0 的顺序推 f[v] ,这样才能保证推 f[v]f[v-c[i]] 保存的是状态 f[i-1][v-c[i]] 的值。伪代码如下:

    for i = [1..N)
        for v = [V..0]
            f[v]=max{f[v],f[v-c[i]]+w[i]};
    

    其中的 f[v] = max{f[v] , f[v-c[i]]} 一句恰就相当于我们的转移方程 f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的 f[v-c[i]] 就相当于原来的 f[i-1][v-c[i]] 。如果将 v 的循环顺序从上面的逆序改成顺序的话,那么则成了 f[i][v]f[i][v-c[i]] 推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解 01 背包问题是十分必要的。

    第一种优化方案 O(n*C) O(2*C)

    因为 dp[i][C] 的值只与 dp[i - 1][C] 有关,所以定义一个 dp[2][C] 来循环保存结果

    不断进行覆盖

    public int knapsack01_ex(int[] w, int[] v, int W) {
        int len = w.length;
        //
        int[][] dp = new int[2][W + 1];
    
        for (int i = 1; i <= len; i++) {
            for (int j = 1; j <= W; j++) {
                //f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]+w[i]}
                //当前最大价值等于放上一件的最大价值
                dp[i % 2][j] = dp[(i - 1) % 2][j];
                //如果当前件(i-1)的重量小于等于总重量,可以放进去或者拿出别的东西再放进去
                if (j >= w[i - 1]) {
                    dp[i % 2][j] = Math.max(dp[(i - 1) % 2][j], v[i - 1] + dp[(i - 1) % 2][j - w[i - 1]]);
                }
            }
        }
    
    
        return dp[len % 2][W];
    }
    

    一维数组 O(n*C) O(C)

    public int knapsack01(int[] w, int[] v, int W) {
        int len = w.length;
        int[] dp = new int[W + 1];
    
        for (int i = 1; i < len; i++) {
            for (int j = W; j >= 0; j--) {
                if (j >= w[i]) {
                    dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
                } else {
                    dp[j] = dp[j];
                }
    
            }
        }
        return dp[W];
    }
    

    事实上,使用一维数组解 01 背包的程序在后面会被多次用到,所以这里抽象出一个处理一件 01 背包中的物品过程,以后的代码中直接调用不加说明。

    过程 ZeroOnePack,表示处理一件 01 背包中的物品,两个参数 costweight 分别表明这件物品的费用和价值。

    procedure ZeroOnePack(cost,weight)
        for v=V..cost
            f[v]=max{f[v],f[v-cost]+weight}
    

    注意这个过程里的处理与前面给出的伪代码有所不同。前面的示例程序写成v=V..0是为了在程序中体现每个状态都按照方程求解了,避免不必要的思维复杂度。而这里既然已经抽象成看作黑箱的过程了,就可以加入优化。费用为cost的物品不会影响状态f[0..cost-1],这是显然的。

    有了这个过程以后,01背包问题的伪代码就可以这样写:

    for i = 1..N
        ZeroOnePack(c[i],w[i]);
    

    初始化的细节问题——“恰好装满背包?”

    求最优解的背包问题中,有两种不太相同的问法:

    • 有的题目要求“恰好装满背包”时的最优解
    • 有的题目则没有要求必须把背包装满

    区别仅在初始化的时候有所不同:

    • 如果是第一种问法,要求恰好装满背包,那么在初始化时除了 f[0]0 其它 f[1..V] 均设为 -∞,这样就可以保证最终得到的 f[N] 是一种恰好装满背包的最优解。

    • 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

    为什么呢?

    • 初始化的 f 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可能被价值为 0nothing “恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是 -∞ 了。
    • 如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

    这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。

    一个常数优化

    前面的伪代码中有 for v=V..1,可以将这个循环的下限进行改进。

    由于只需要最后 f[v] 的值,倒推前一个物品,其实只要知道 f[v-w[n]] 即可。以此类推,对以第 j个背包,其实只需要知道到 f[v-sum{w[j..n]}] 即可,即代码中的

    for i=1..N
        for v=V..0
    

    可以改成

    for i=1..n
        bound=max{V-sum{w[i..n]},c[i]}
        for v=V..bound
    

    这对于 V 比较大时是有用的。

    小结

    01 背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成 01 背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。

    完全背包问题

    多重背包问题

    混合三种背包问题

    参考

    动态规划问题——0/1背包问题(Java实现)

  • 相关阅读:
    谣言检测——(PSA)《Probing Spurious Correlations in Popular EventBased Rumor Detection Benchmarks》
    谣言检测(GACL)《Rumor Detection on Social Media with Graph Adversarial Contrastive Learning》
    谣言检测——(BiGCN)《Rumor Detection on Social Media with BiDirectional Graph ConvolutionalNetworks》
    谣言检测()——《Debunking Rumors on Twitter with Tree Transformer》
    谣言检测——(GLAN)《Jointly embedding the local and global relations of heterogeneous graph for rumor detection》
    谣言检测——(EBGCN)《Towards Propagation Uncertainty: Edgeenhanced Bayesian Graph Convolutional Networks for Rumor Detection》
    代码
    第五章学习笔记
    cat userlist
    20201307梁辰鱼第11章学习总结
  • 原文地址:https://www.cnblogs.com/xiehang/p/11329516.html
Copyright © 2020-2023  润新知