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

本文是Java入门算法系列的动态规划篇,重点讲解01背包问题。通过简化变量名,清晰地介绍了如何用动态规划解决01背包问题,包括问题描述、分析及状态转移方程,并提及了如何通过滚动数组优化空间复杂度。
摘要由CSDN通过智能技术生成

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

往期文章

一. 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

    • 5
      点赞
    • 6
      收藏
      觉得还不错? 一键收藏
    • 打赏
      打赏
    • 6
      评论
    评论 6
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    莉妮可丝的猫

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值