代码随想录 day 35 动态规划

第九章 动态规划part03

正式开始背包问题,背包问题还是挺难的,虽然大家可能看了很多背包问题模板代码,感觉挺简单,但基本理解的都不够深入。
如果是直接从来没听过背包问题,可以先看文字讲解慢慢了解 这是干什么的。
如果做过背包类问题,可以先看视频,很多内容,是自己平时没有考虑到位的。
背包问题,力扣上没有原题,大家先了解理论,今天就安排一道具体题目。
掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。
详细布置

01背包问题 二维

https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html
视频讲解:https://www.bilibili.com/video/BV1cg411g7Y6

01背包问题 一维

https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html
视频讲解:https://www.bilibili.com/video/BV1BU4y177kY

01背包总结

416. 分割等和子集

本题是 01背包的应用类题目
https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html
视频讲解:https://www.bilibili.com/video/BV1rt4y1N7jE

01背包

题目链接

https://kamacoder.com/problempage.php?pid=1046

解题思路

什么是01背包?

背包问题的理论基础重中之重是01背包,一定要理解透!

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

自底向上思考,暴力解法如何做?

每一件物品只有俩个状态,取或不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),n表示物品数量
所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!

public class BagProblem {

    /**
     * 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。
     * 每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
     *
     */
    int maxValue=0;
    List<Integer> bagLog =new ArrayList<>();
    int curWeight=0;
    int curValue=0;
    public void backtracking(int bagSize,int [] weight,int [] value,int startIndex){
        if(curWeight>bagSize){
            return;
        }
        //遍历物品
        for(int i=startIndex;i<weight.length;i++){
            if(curWeight+weight[i]>bagSize){
                break;
            }
            bagLog.add(i);
            curValue+=value[i];
            curWeight+=weight[i];
            System.out.println("当前背包的物品:"+ bagLog.toString() +"价值:"+curValue);
            maxValue=Math.max(maxValue,curValue);
            backtracking(bagSize,weight,value,i+1);
            if(bagLog.size()==weight.length){
                return;
            }
            curValue-=value[i];
            curWeight-=weight[i];
            bagLog.remove(bagLog.size()-1);
        }
    }

    public  int bagProblemWithBacktracking(int bagSize,int [] weight,int [] value){
        backtracking(bagSize,weight,value,0);
        return maxValue;
    }

    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 10;
        BagProblem bagProblem = new BagProblem();
        int _maxValue = bagProblem.bagProblemWithBacktracking(bagSize, weight, value);
        System.out.printf(""+_maxValue);
    }

}
二维dp数组01背包

:::tips
动规五步曲分析
1.确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
二维数组如图所示,要时刻记着dp数组的含义,i代表什么 j代表什么

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

进一步理解疑惑,这里为什么取最大,为什么j-weight[i]?
因为这里放物品i 不是说你的背包是无限大的,直接就放进去,比如:你背包重量是5,当前背包有
i1 重量是1 i2 重量是2 i3重量是1 ,此时来到背包重量5,物品i4的重量是4,你要想能放物品i4,n你就得j-weight[i4] =1 , 那么现在背包有物品i1 + 物品i4 ,这时候就要比较 i1,i4的价值大还是i1,i2,i3的价值大,是不确定的,所以要取他俩的最大值。
j - weight[i] 的意思是要确保能放进去 weight[i] 才能加入背包,也就是背包容量为j - weight[i]的价值是多少 再加上 value[i] ,当然你能不能让数组越界 j - weight[i] <0 根本就放不下这个物品,还是要取dp [i-1] [j]
3.dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
3.1首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0

3.2在看其他情况,状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化
** j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小**
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品

所以初始化如图

4.确定遍历顺序
如上图所示有俩个遍历维度,物品与背包重量
遍历物品更好理解,因为逻辑上是取物品放到背包里面嘛,但在二维里都可以。
为什么都可以的呢?
要理解递归的本质和递推的方向
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]的左上角方向(包括正上方向)

无非就竖或横着填充二维数组,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的取值 推导!

5.举例推导dp数组

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

总结:
做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!
:::

public class BagProblem {

    public int bagProblem(int bagSize,int[] weight,int[] value){
        //1.确定dp数组以及下标的含义
        //dp[i][j] 任取0-i物品放进重量为j的背包的最大价值
        //2.确定递推公式
        //dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
        //3.如何初始化dp数组
        int dp[][]=new int[weight.length][bagSize+1];//weight.length 物品大小
        //3.1 背包重量是0的时候放不下物品 初始化价值为0, java int数组默认是0
        //3.2 递推公式由i-1推出,要初始化dp[0,j]的值
        for (int j = 0; j <= bagSize ; j++) {
            if(j>=weight[0]){
                dp[0][j]=value[0];
            }
        }
        //4.确定遍历顺序
        for(int i=1;i<weight.length;i++){//遍历物品
            for(int j=1;j<=bagSize;j++){//遍历背包重量
                //处理 j-weight[i] 小于0的情况放不下物品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]);
                }
            }
        }
        //5.打印dp数组
        for(int i=0;i<weight.length;i++){
            for(int j=0;j<=bagSize;j++){
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println();
        }
        //最后一个位置就是答案
        return dp[weight.length-1][bagSize];
    }

    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        int maxValue = new BagProblem().bagProblem(bagSize, weight, value);

        System.out.println("最大值:"+maxValue);
    }
}

image.png

滚动数组(一维dp数组)01背包

:::tips
滚动数组就是把二维dp降为一维dp
image.png
对于背包问题其实状态都是可以压缩的。
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
动规五步曲
1.确定dp数组的定义以及下标的含义
dp[j] 表示容量为j的背包,所背的物品价值最大为dp[j]
2.一维数组的递推公式
二维递推公式
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
一维dp数组,其实就是上一层dp[i-1] 这一层拷贝到dp[i]
dp[j]=max(dp[j],dp[j-weight[i]] + value[i])
此时dp[j]有两个选择:
一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i
一个是取dp[j - weight[i]] + value[i],即放物品i
指定是取最大的,毕竟是求最大价值。
3.一维dp数组的初始化
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

4.一维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
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
二维不用倒序是因为dp[i][j]都是通过上一层dp[i-1][j]而来

为什么不可以先遍历背包在遍历物品?
因为背包容量是倒序遍历,如果这样背包里就会只放一个物品,举个例子试下就知道了
例如:遍历背包容量第一次取容量是4, 取物品0放入, 取物品1 dp[j-weight[1]] 它是空值 +value[i],此时背包就只有物品i ,因为是从后向前,前面不可能有值,最终背包容量是4的,只有一个价值最大的物品是不对的。

5.举例推导dp数组

:::

public class BagProblem {

    public int bagProblem(int bigSize,int[] weight,int[] value){

        //1.确定dp数组以及下标的含义
        //dp[j] 容量为j的背包,所背物品的最大价值为dp[j]
        //2.确定递推公式
        //dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])
        //3.初始化
        int [] dp=new int[bigSize+1];
        Arrays.fill(dp,0);

        //4.确定遍历顺序 先遍历物品在遍历背包,背包要倒序
        for(int i=0;i<weight.length;i++){//遍历物品
            //for(int j=bigSize;j>=weight[i];j--){
            //    dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
            //}
            //方便打印全部dp数组,实际写用上面注释的遍历方式
            for(int j=bigSize;j>=0;j--){//遍历背包重量
                if(j<weight[i]){//确保要当前背包可以装的下物品i
                    dp[j]=dp[j];//这里不用写,装不下物品i了,因为是滚动数组,维持原来的物品,上面的循环省去了额外遍历还是有好处的
                }else {
                    dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
                }
                System.out.print(dp[j]+"\t");
            }
            System.out.println();
        }

        //5.举例推导dp数组


        return dp[bigSize];

    }

    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        int maxValue = new BagProblem().bagProblem(bagSize, weight, value);

        System.out.println("最大值:"+maxValue);
    }

}

这里打印出的背包遍历顺序是4-0 ,下图打印的第一个位置就是dp数组的4号位置,最后一个位置是dp数组的0号位置,因为打印先处理的背包4号位置->3…0。
解释的意思是不要产生误会,看打印然后取dp[0],实际取dp[4] 就是dp[bigSize]。
image.png
for(int j=bigSize;j>=weight[i];j–) 这个循环明显执行次数更少
image.png

正常想的模拟图应该这么画,实际对应dp数组打印的翻转,你应该懂的!

416. 分割等和子集

题目链接

https://leetcode.cn/problems/partition-equal-subset-sum/description/

解题思路

要明确本题中我们要使用的是01背包,因为元素我们只能用一次。
回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。
那么来一一对应一下本题,看看背包问题如何来解决。

只有确定了如下四点,才能把01背包问题套到本题上来。
背包的体积为sum / 2
背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
背包如果正好装满,说明找到了总和为 sum / 2 的子集。
背包中每一个元素是不可重复放入。

code

class Solution {
    // 只有确定了如下四点,才能把01背包问题套到本题上来。

    // 背包的体积为sum / 2
    // 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
    // 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
    // 背包中每一个元素是不可重复放入。
    public boolean canPartition(int[] nums) {
        int sum=Arrays.stream(nums).sum();
        if(sum%2==1){
            return false;
        }
        //1.确定dp数组以及下标的含义
        //01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。
        //j 是sum/2 物品就是子集
        //2.确定递推公式
        //01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        //本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]
        //dp[j]=Math.max(dp[j],dp[j=nums[i]+nums[i]]);
        //3.dp数组如何初始化
        int bagSize=sum/2;
        int[] dp=new int[bagSize+1];
        Arrays.fill(dp,0);
        //4.确定遍历顺序
        for(int i=0;i<nums.length;i++){
            for(int j=bagSize;j>=nums[i];j--){
                dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        //dp[bagSize] 计算出背包的最大数值,必定是装满 
        //题目要求俩个子集相等,sum/2代表,俩个子集的值
        //那么sum/2=bagSize = dp[bagSize] 就说明存在这俩个子集
        return dp[bagSize]==bagSize;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值