【随想录】Day42—第九章 动态规划part04


题目1: 01 背包理论基础(二维)


1- 01 背包

  • 有 n 件物品和一个最多能背重量为 w 的背包。第 i 件物品的重量是 weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
  • 这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。
  • 这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?
  • 每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的 n 表示物品数量。
  • 所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!

在下面的讲解中,我举一个例子:

  • 背包最大重量为4。
  • 物品为:

image.png

  • 问背包能背的物品最大价值是多少?
  • 以下讲解和图示中出现的数字都是以这个例子为例。

2- 动规:二维 dp 数组解决 01 背包

依然动规五部曲分析一波。

  • 1. 确定dp数组以及下标的含义
    • 对于背包问题,有一种写法, 是使用二维数组,即 dp[i][j] 表示从下标为[0-i] 的物品里任意取,放进容量为 j 的背包,价值总和最大是多少
    • 只看这个二维数组的定义,大家一定会有点懵,看下面这个图:

image.png

  • 要时刻记着这个dp数组的含义,下面的一些步骤都围绕这 dp 数组的含义进行的,如果哪里看懵了,就来回顾一下i代表什么, j 又代表什么。

  • 2. 确定递推公式
    • 再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
    • 那么可以有两个方向推出来 dp[i][j]
    • 不放物品i:由 dp[i - 1][j] 推出,即背包容量为 j,里面不放物品 i 的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
    • 放物品i:由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的价值),就是背包放物品i得到的最大价值
  • 所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

  • 3. dp数组如何初始化
  • 关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱
  • 首先从dp[i][j]的定义出发,如果背包容量 j 为 0 的话,即 dp[i][0],无论是选取哪些物品,背包价值总和一定为 0 。如图:

image.png
在看其他情况。
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出 i 是由 i-1 推导出来,那么i0的时候就一定要初始化。
dp[0][j],即:i0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
代码初始化如下:

for (int j = 0 ; j < weight[0]; j++) {  // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
    dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}

此时dp数组初始化情况如图所示:

其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。
初始-1,初始-2,初始100,都可以!
但只不过一开始就统一把dp数组统一初始为0,更方便一些。
如图:
image.png


  • 4. 确定遍历顺序
  • 由于 dp 数组横向是 背包容量 纵向是 物品
  • 这里采用 **_先遍历物品重量,再遍历背包容量 _**的方式进行遍历
    • 遍历物品:由于此时下标为 0 的物品已经初始化过,所以 i 从 1 开始遍历 物品
    • 遍历背包:此时遍历背包的大小,则 j 从 0 开始遍历,遍历到背包的容量即 j <= bagweight
    • ① 如果 当前背包没有物品重,此时 dp[i][j] 应该为 dp[i][j] = dp[i - 1][j]
    • ② 否则 将 i 放入背包,此时 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    }
}

  • 5. 举例推导 dp 数组

image.png

  • 最终结果就是dp[2][4]。

3- 题解

⭐ 01 背包理论基础——题解思路

在这里插入图片描述

import java.util.Scanner;
public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();

        // 1. 定义 dp 数组
        int[][] dp = new int[m][n+1];

        int[] weight = new int[m];
        for(int i = 0 ; i < m;i++){
            weight[i] = sc.nextInt();
        }
        int[] value = new int[m];
        for(int i = 0 ; i < m;i++){
            value[i] = sc.nextInt();
        }

        // 2. 递推公式 dp[i][j]
        // 2.1 如果当前 (背包容量) < (物品重量) 即 j < weight[i] ——> dp[i][j] = dp[i-1][j]
        // 2.2 否则 dp[i][j] 为  Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);

        // 3. 初始化
        // 3.1 初始化第一列
        for(int i = 0 ; i < m;i++){
            dp[i][0] = 0;
        }

        // 3.2 初始化第一行
        for(int i = 0 ; i <= n;i++){
            if(i>=weight[0]){
                dp[0][i] = value[0];
            }
        }

        // 4. 遍历顺序
        // 先遍历 物品 再遍历 背包
        for(int i = 1 ; i < m;i++){
            for(int j = 1 ; j<=n;j++){
                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]);
                }
            }
        }
        
        System.out.println(dp[m-1][n]);
//        return dp[m-1][n];
    }
}

题目2: 01 背包理论基础(一维)


1- 思路

动规五部曲

  • 1. 定义 dp 数组以及其含义
    • dp[j] 代表 容量为 j 的背包的最大价值
  • 2. 递推公式
    • dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
  • 3. dp 数组初始化
    • dp 数组初始化为非 0 中的最小值
  • 4.确定遍历顺序
    • 先遍历背物品: 商品从 i = 0 开始 遍历到 m
    • 再遍历背包:背包从 j = 背包大小 开始 遍历到 weight[i]

2- 题解

⭐ 01 背包理论基础(一维)——题解思路

在这里插入图片描述

import java.util.Scanner;
public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();

        // 1. 定义 dp 数组
        int[] dp = new int[n+1];

        int[] weight = new int[m];
        for(int i = 0 ; i < m;i++){
            weight[i] = sc.nextInt();
        }
        int[] value = new int[m];
        for(int i = 0 ; i < m;i++){
            value[i] = sc.nextInt();
        }

        // 2. 递推公式 dp[i][j]

        // 3. 初始化
        dp[0] = 0;

        // 4. 遍历顺序
        // 先遍历 物品 再遍历 背包
        for(int i = 0 ; i < m;i++){
            for(int j = n ; j>=weight[i];j--){
                dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i]);
            }
        }
        System.out.println(dp[n]);
//        return dp[m-1][n];
    }
}

题目3: 分割等和子集


1- 思路

数组分割为背包问题,比如求 [1,5,11,5] 数组中是否能分为两个和为 11 的数组。

  • 可以将该问题抽象为 : 容量为 11 的背包能不能装满

动规五部曲

  • 1. 定义 dp 数组确定 dp 数组含义
    • int[] dp :代表容量为 j 的数组包含的最大价值
    • 题目提供的数组 即代表容量 也代表最大价值
    • 本题的返回条件dp[target] == target 此时返回 true
    • target的求法:先对数组求和,对求和结果 / 2 得到的就是 target
  • 2. 确定递推公式,即状态转移方程
    • dp[j] = Math.max(dp[j],dp[j-weight[i]] + value[j])
    • 此时状态转移方程类似于 0 1 背包问题中的压缩方式
  • 3. 初始化 dp 数组
    • dp[0] =0 其他是非负数的最小值 也就是 0
  • 4. 确定遍历顺序
    • 4.1 先遍历商品
      • i 从 0 到 n
    • 4.2 再遍历背包
      • j 从 target 到 weight[i] ,此时商品的重量和价格看作是一样的
      • 背包需要反向遍历,因为每个元素我仅使用一次

2- 题解

⭐ 分割等和子集——题解思路

在这里插入图片描述

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        int sum = 0;
        for(int a:nums){
            sum+=a;
        }

        //总和为奇数,不能平分
        if(sum % 2 != 0) return false;
        int target = sum / 2;

        // 1. 定义 dp数组,含义:容量为 j 的背包最大的价值
        int[] dp = new int[target+1];


        // 2. 递推公式 dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
        // 3. 初始化dp 数组
        dp[0] = 0;

        // 4. 遍历顺序
        for(int i = 0 ; i < n;i++){
            for(int j = target;j >= nums[i];j--){
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
                if(dp[j] == target){
                    return true;
                }
            }
        }

        return false;
    }
}
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值