算法Day31|动态规划专题三 01背包问题 (二维数组), 01背包问题 (一维数组),416. 分割等和子集,1049. 最后一块石头的重量 II

本文详细解析了01背包问题,包括二维数组和一维数组的实现方法,并进一步拓展介绍了分割等和子集及最后一块石头的重量II等问题的解决思路。

 01背包问题 (二维数组) 

1.题目描述

  • 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大:

动态规划-背包问题

  • 在下面的讲解中,我举一个例子:
  • 背包最大重量为4。
  • 物品为:

2.解题思路 

  • 动规五部曲:
  1. 确定dp数组以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组
  •  1.确定dp数组以及下标的含义
    •  对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
    • 只看这个二维数组的定义,大家一定会有点懵,看下面这个图:

动态规划-背包问题1

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数组如何初始化
    • 初始化第一列:背包容量为0时,价值都为0

    • 初始化第一行:
      • 只有一个商品(它的重量为 weight[0])时,背包容量大于该商品重量时,价值为value[0],否则为0
        ​​

动态规划-背包问题7

  • 4.确定遍历顺序
  • 在如下图中,可以看出,有两个遍历的维度:物品与背包重量
  • 那么问题来了,先遍历物品还是先遍历背包重量呢?
  • 其实都可以!! 但是先遍历物品更好理解

动态规划-背包问题3

  • 要理解递归的本质和递推的方向
  • dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
  • 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。
  • dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向)
  • 那么先遍历物品,再遍历背包的过程如图所示:

动态规划-背包问题5

5.举例推导dp数组

  • 来看一下对应的dp数组的数值,如图:

动态规划-背包问题4

3.代码实现

//01背包问题 二维数组
    public static void getMaxValueFromBag(int[] weight, int[] value, int bagSize) {
        //商品个数wLen
        int wLen = weight.length;
        //i为行,为商品的个数
        //j为列,为背包容量,可以取到bagSize
        //dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
        int[][] dp = new int[wLen][bagSize + 1];
        //初始化第一列:背包容量为0时,价值都为0
        for (int i = 0; i < wLen; i++) {
            dp[i][0] = 0;
        }
        //初始化第一行:只有一个商品(它的重量为 weight[0])时,
        //背包容量大于该商品重量时,价值为value[0],否则为0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }
        for (int i = 1; i < wLen; i++) {//遍历物品
            for (int j = 1; j <= bagSize; j++) {// 遍历背包容量
                //当前商品的重量比背包容量大
                if (weight[i] > j) {
                    //当前商品放不下,从i-1中取商品,容量为j
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //不放物品i:dp[i - 1][j]
                    //放物品i: dp[i - 1][j - weight[i]] + value[i]
                    //取两者最大值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }
        System.out.println(Arrays.deepToString(dp));
    }

01背包问题 (一维数组)

1.解题思路 

  •  1.确定dp数组以及下标的含义
    • dp[j]的含义:从容量为j的背包,价值总和最大是多少
  • 2.确定递推公式
    • 不放物品i:  dp[j]
    • 放物品i:      dp[j - weight[i]] + value[i]
    • 取两者最大值
    • dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
  • 3.dp数组如何初始化
    • 初始化:背包容量为0时,价值都为0

    • dp[0]=0

4.确定遍历顺序

        for (int i = 0; i < wLen; i++) {//遍历物品
            for (int j = bagSize; j >= weight[i]; j--) {// 遍历背包容量
                //不放物品i:dp[j]
                //放物品i: dp[j - weight[i]] + value[i]
                //取两者最大值
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
  • 这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!
  • 二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
  • 为什么呢?
  • 倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
  • 举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
    • 如果正序遍历
    • dp[1] = dp[1 - weight[0]] + value[0] = 15
    • dp[2] = dp[2 - weight[0]] + value[0] = 30
    • 此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
  • 为什么倒序遍历,就可以保证物品只放入一次呢?
    • 倒序就是先算dp[2]
    • dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
    • dp[1] = dp[1 - weight[0]] + value[0] = 15
  • 所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
  • 5.举例推导dp数组
    • 一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:

动态规划-背包问题9

2.代码实现

    //01背包问题 一维数组
    public static void getMaxValueFromBag2(int[] weight, int[] value, int bagSize) {
        //商品个数wLen
        int wLen = weight.length;
        //dp[j]的含义:从容量为j的背包,价值总和最大是多少。
        int[] dp = new int[bagSize + 1];
        //初始化:背包容量为0时,价值都为0
        dp[0] = 0;
        for (int i = 0; i < wLen; i++) {//遍历物品
            for (int j = bagSize; j >= weight[i]; j--) {// 遍历背包容量
                //不放物品i:dp[j]
                //放物品i: dp[j - weight[i]] + value[i]
                //取两者最大值
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        System.out.println(Arrays.toString(dp));
    }

416. 分割等和子集

1.题目描述

  • 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

 ​​​​​

 2.解题思路 

  •  1.确定dp数组以及下标的含义
    •   dp[j]定义:背包总容量是j,最大可以凑成j的子集总和为dp[j]
  • 2.确定递推公式
    • 不放物品i:  dp[j]
    • 放物品i:      dp[j - nums[i]] + nums[i]
    • 取两者最大值
    • dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
  • 3.dp数组如何初始化
    • 初始化:背包容量为0时,放进数字为0

    • dp[0]=0

  • 4.确定遍历顺序
    • 和01背包问题 (一维数组)一样。
  • 5.举例推导dp数组
    • dp[j]的数值一定是小于等于j的。
    • 如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
    • 用例1,输入[1,5,11,5] 为例,如图:

416.分割等和子集2

最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。 

  3.代码实现

class Solution {
   //416. 分割等和子集
    public boolean canPartition(int[] nums) {
        int target = 0;
        for (int num : nums) {
            target += num;
        }
        if (target % 2 != 0) return false;//总和为奇数,不能平分
        target = target / 2;//target为总和的一半
        //dp[j]定义:背包总容量是j,最大可以凑成j的子集总和为dp[j]。
        int[] dp = new int[target + 1];
        for (int i = 0; i < nums.length; i++) {//遍历物品
            for (int j = target; j >= nums[i]; j--) {// 遍历背包容量
                //物品 i 的重量是 nums[i],其价值也是 nums[i]
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[target] == target;
    }
}

1049. 最后一块石头的重量 II 

 1.题目描述

  • 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
  • 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
    • 如果 x == y,那么两块石头都会被完全粉碎;
    • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
  • 最后,最多只会剩下一块 石头。返回此石头最小的可能重量 。如果没有石头剩下,就返回 0。

 ​​​​​

 2.解题思路 

  •  1.确定dp数组以及下标的含义
    •   dp[j]定义容量为j的背包,最多可以背dp[j]这么重的石头。
  • 2.确定递推公式
    • 不放物品i:  dp[j]
    • 放物品i:      dp[j - stones[i]] + stones[i]
    • 取两者最大值
    • dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
  • 3.dp数组如何初始化
    • 初始化:背包容量为0时,放进数字为0

    • dp[0]=0

  • 4.确定遍历顺序
    • 和01背包问题 (一维数组)一样。
  • 5.举例推导dp数组
    • 举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

1049.最后一块石头的重量II

  • 最后dp[target]里是容量为target的背包所能背的最大重量。
  • 那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
  • 在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的
  • 那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

  3.代码实现

class Solution {
    //1049. 最后一块石头的重量 II
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int stone : stones) {
            sum += stone;
        }
        int target = sum / 2;//target为总和的一半
        //dp[j]定义:容量为j的背包,最多可以背dp[j]这么重的石头。
        int[] dp = new int[target + 1];
        for (int i = 0; i < stones.length; i++) {//遍历物品
            for (int j = target; j >= stones[i]; j--) {// 遍历背包容量
                //物品 i 的重量是 nums[i],其价值也是 nums[i]
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - dp[target] - dp[target];
    }
}

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值