【仓鼠啃算法】动态规划之“01背包”问题(1)

1、问题描述

有N件物品,一个最多承重为W的背包。

给出一个 weight[] 数组和一个 value[] 数组,weight[i] 表示第 i 件物品的重量,value[i] 表示第 i 件物品的价值。

每件物品只有一个,且只能整体使用,不能分割。求解,将哪些物品装入背包后,总价值最大?

2、问题分析

注意,这个背包问题的要求是,每件物品只有一个,且只能整体使用。它还有两个兄弟姐妹:

  • 如果物品可以分割成小份,就属于“贪心背包”。做法是,计算每个物品的“性价比”,然后从大到小装满背包即可。

  • 如果物品数量无限,就属于“完全背包”问题。

暴力解法

来分析问题。每件物品只有两种可能性:取或者不取。可以使用回溯算法,搜索所有的情况,记录最大的价值即可。

时间复杂度是O(2n),需要使用动态规划来优化。

3、二维 dp 数组

DP 五部曲:

  1. 确定 dp 数组及下标的含义。

    dp[i][j] 表示,从下标为 [0~i] 的物品中,取任意物品放入容量为 j 的背包的价值总和(这些物品会使得背包的价值最大)

    注意 j 不是剩余容量,而是当前假设背包的总容量

  2. 确定递推公式

    可以从两个方向推导 dp[i][j]:

    • 由 dp[i-1][j] 推出,即背包容量为 j ,里面不放物品 i 的最大价值,此时 dp[i][j] 就是 dp[i-1][j]

    • 由 dp[i-1] [ j-weight[i] ] 推出,dp[i-1] [ j-weight[i] ] 是当背包容量为 j-weight[i] 时,不放物品 i 的最大价值

      那么 dp[i-1] [ j-weight[i] ] + value[i] 就是背包放入物品 i 后得到的最大价值

    所以递推公式是:dp[i][j] = Math.max(dp[i-1][j], dp[i-1] [ j-weight[i] ] + value[i] ) 其实就是看一看,放入 i 这个物品到底好不好

  3. 初始化 dp 数组

    • 如果容量为0,那么价值肯定就是0,所以每个 dp[i][0] = 0

    • 递推公式用到了 i-1,所以 dp[0][j] 也必须初始化。它的含义是,各个容量存放0号物品的情况(能放下,价值就是0号的价值。放不下,价值就为0)

  4. 确定遍历顺序

    遍历有两个维度:物品、背包重量。

    其实从哪个先遍历都可以,这是因为需要的历史数据都来源于 dp[i][j] 的左上角。只要不影响dp的推导,那么怎么遍历都可以。

  5. 举例推导 dp 数组

如何理解这个状态转移公式

首先明确,dp[i][j] 的含义是:容量为 j 的背包,从 0~i 号物品中,挑选几个放入背包,能得到的最大价值。

接着,如果当前背包的容量 j > weight[i],即这个物品能放入背包中,考虑要不要放入它:

  • 如果不放入,则背包变成了“从 0~i 号物品中选择,它的容量是 j,它的价值不变” (之前的状态是,从 0~i-1 号物品中选择,容量为 j )

  • 如果选择放入,则背包变成了“从 0~i 号物品中选择,它的容量是 j-weight[i],它的价值增加了value[i]”

我们取这两种状态的最大值,这就是处置物品 i 的最优策略。再来看状态转移公式,就很明了:

dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);

接着考虑,为什么这两种状态的值,都已经存在于dp数组中?

  • 对于物品的范围,相当于在同一列上做取舍,且依赖的是“靠近上面”的值

  • 对于背包的容量,相当于在同一行上做取舍,且依赖的是“靠近左边”的值

我们只需要将第一行(不同容量的背包要装0号物品),和第一列(容量为0的背包要装每个物品)的值进行初始化,后面的值就能依次计算出来。

代码实现

public class Bag {
    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWeight = 4;
        domain(weight, value, bagWeight);
    }
​
    public static void domain(int[] weight, int[] value, int bagWeight){
        //dp数组
        int[][] dp = new int[value.length][bagWeight + 1];
​
        //初始化
        for (int j = weight[0]; j <= bagWeight; j++){
            dp[0][j] = value[0];
        }
​
        //先遍历物品再遍历背包
        for (int i = 1; i < value.length; i++){
            for (int j = 0; j <= bagWeight; j++){
                //如果剩余容量装不下物品i,那就不装
                if (j < weight[i]){
                    dp[i][j] = dp[i-1][j];
                }else{
                    //能装得下,考虑是否要装它
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }
​
        //dp数组构建完成,给出给定物品个数和背包容量的最大值
        System.out.println(dp[value.length-1][bagWeight]);
​
        //打印dp数组
        for (int i = 0; i < value.length; i++){
            System.out.print(i+"号物品:   ");
            for (int j = 0; j <= bagWeight; j++){
                System.out.print(dp[i][j] + " ");
            }
            System.out.println();
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值