Java入门算法(动态规划篇2:01背包精讲)

Posted 卉卉今天吃什么

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java入门算法(动态规划篇2:01背包精讲)相关的知识,希望对你有一定的参考价值。

本专栏已参加蓄力计划,感谢读者支持❤

往期文章

一. Java入门算法(贪心篇)丨蓄力计划
二. Java入门算法(暴力篇)丨蓄力计划
三. Java入门算法(排序篇)丨蓄力计划
四. Java入门算法(递归篇)丨蓄力计划
五. Java入门算法(双指针篇)丨蓄力计划
六. Java入门算法(数据结构篇)丨蓄力计划
七. Java入门算法(滑动窗口篇)丨蓄力计划
八. Java入门算法(动态规划篇1:初识动规)
九. Java入门算法(动态规划篇2:01背包精讲)



01背包

网上有非常多的文章对01背包进行讲解,变量名繁杂,对初学者不怎么友好。在这篇文章里,我尽量讲得简单,不作一些多余的赘述。

不会有人不知道背包是什么吧?


问题描述

       把n种物品装进一个背包,物品 i 的重量是w[ i ],价值是c[ i ],背包的容量是M,求能装进背包的最大总价值。

注:w是存储n个物品的重量的数组,c是存储n个物品的价值的数组


分析

为啥叫01背包呢?因为对于每件物品,只有 不拿(0) 与 拿(1) 两种状态。

动态规划解01背包,关键是填二维表dp:

  • 行 i 指的是我们当前要考虑装 i 个物品
  • 列 j 表示的是背包剩余容量
  • dp [i] [j] 的值是指 i 个物品装进容量为 j 的背包的最大总价值

当 i 等于物品总量n、j 等于背包容量M的时候,dp [i] [j] 的值就是问题的解。下面根据例子输入,边填表边分析。

输入:n = 4, M = 10
w[] = [2, 3, 4, 7]
c[] = [1, 3, 5, 9]
(4个物品装进容量为10的背包)
  • 初始化第0行为全0,没有物品,不管容量多大,价值只能是0
  • 初始化第0列为全0,容量为0,装不下任何物品,价值只能是0


现在只考虑装 1 件物品(i = 1)

  • dp[1][1] = 0,容量为1的背包装不下第 1 件物品,因为它的重量为2


  • dp[1][2] = 1,此时容量为2的背包可以装下物品1,而它的价值为1


    • 第1行后面容量>1的背包,都可以装下物品1,因此它们的价值都为1


    现在考虑的是装 2 件物品(i = 2)

    • dp[2][1] = 0,容量为1的背包即装不下物品1(重量2)也装不下物品2(重量3)


    • 容量为2的背包装不下物品2,但可以装下物品1
    • 因此dp[2][2] = dp[1][2] = 1


    此时引出第一种情况:

    1. 当背包容量小于物品 i 的重量 w[i] 时,拿不了物品 i ,所以考虑拿 i - 1个物品的最大总价值。
    2. 得到状态转移方程: dp[i][j] = dp[ i - 1][j] ( j < w[i])
           if (j < w[i]) {
               dp[i][j] = dp[i - 1][j];
           }
    

    • 容量为3的背包既可以装下物品2、也可以装下物品1。要使价值最大,我们肯定会选装物品2,这样就是dp[2][3] = 3。
    • 此时的背包容量都用来装物品2,已经满了,装不下物品1(对应dp[1][0] = 0)。
    • 但这只是我们主观得出的选择,计算机怎么知道是选装物品1还是物品2呢?

    此时引出第二种情况:

    1. 当背包容量大于物品 i 的重量 w[i] 时,可以拿物品 i ,此时需要考虑拿了物品 i 的价值大,还是不拿物品 i 的价值大
    2. 不拿物品 i ,就是第一种情况,考虑拿 i - 1个物品的最大总价值
    3. 拿物品 i ,就需要把物品 i 装入到背包(+c[i])。但!还需要考虑装完物品 i 的背包还可以装多少(+ dp[ i - 1][ j - w[i]],即上述的 dp[1][0])
    4. 得到状态转移方程: dp[i][j] = c[i] + dp[ i - 1][ j - w[i]] ( j >= w[i])

    如下,容量为5的背包,两个物品都可以装下

    所以dp[2][5] = c[2] + dp[1][5 - 3] 即 4 = 3 + 1

       if (j >= w[i]) {
           dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
       }
    

    综上,可以得到总的状态转移方程:

    d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , ( j < w [ i ] ) dp[i][j] = dp[ i - 1][j] ,( j < w[i]) dp[i][j]=dp[i1][j](j<w[i])
    d p [ i ] [ j ] = c [ i ] + d p [ i − 1 ] [ j − w [ i ] ] , ( j > = w [ i ] ) dp[i][j] = c[i] + dp[ i - 1][ j - w[i]], ( j >= w[i]) dp[i][j]=c[i]+dp[i1][jw[i]](j>=w[i])
    状态转移方程代表了我们接下来的选择,根据它计算出表内的所有值,如下图

    时间复杂度为O(n2),空间复杂度为O(M*n)


    得到题目答案:12

    完整代码

        // 二维dp
        public static int dp_2d(int n, int[] w, int[] c, int M) {
            int[][] dp = new int[n + 1][M + 1];
            for (int i = 1; i < n + 1; ++i) {
                for (int j = 1; j < M + 1; ++j) {
                    if (j < w[i]) {
                        dp[i][j] = dp[i - 1][j];
                    }else{
                        dp[i][j] = Math.max(dp[i - 1][j], c[i] + dp[i - 1][j - w[i]]);
                    }
                }
            }
            return dp[n][M];
        }
    

    进阶

    滚动数组

    • 由上述的填表顺序可以发现,每次 dp[i][j] 的值都是由 i - 1 行左上方或正上方的dp值得出。因此可以尝试把 [i] 这一维度去除,二维dp降成一维dp,通过不断刷新一维dp表的值,模拟上述二维dp的填表过程。时间复杂度仍为O(n^2^),空间复杂度降为O(M)。
    • 但会发现若按照从左往右的顺序填表,会把需要用到的值覆盖,固填表方向需要反过来。


    完整代码

        // 二维降一维,滚动数组
        public static int dp_1d(int n, int[] w, int[] c, int M) {
            int[] dp = new int[M + 1];
            for (int i = 1; i < n + 1; ++i) {
                for (int j = 1; j < M + 1; ++j) {
                    if (j >= w[i]) {
                        dp[j] = Math.max(dp[j], dp[j - w[i]] + c[i]);
                    }
                }
            }
            return dp[M];
        }
    

    END

    参考资料:
    https://www.bilibili.com/video/BV1C7411K79wfrom=search&seid=9137630755457139754

    以上是关于Java入门算法(动态规划篇2:01背包精讲)的主要内容,如果未能解决你的问题,请参考以下文章

    动态规划入门——动态规划与数据结构的结合,在树上做DP

    挑战程序设计竞赛(算法和数据结构)——17.2 01背包问题(动态规划)的JAVA实现

    [动态规划与回溯算法] 01背包

    Java入门算法(动态规划篇1:初识动规)

    动态规划入门:01背包问题

    [动态规划与回溯算法] 01背包