Day34 力扣动态规划 : 01背包问题 二维 |01背包问题 一维 |416. 分割等和子集

正式开始背包问题,背包问题还是挺难的,虽然大家可能看了很多背包问题模板代码,感觉挺简单,但基本理解的都不够深入。

如果是直接从来没听过背包问题,可以先看文字讲解慢慢了解 这是干什么的。

如果做过背包类问题,可以先看视频,很多内容,是自己平时没有考虑到位的。

背包问题,力扣上没有原题,大家先了解理论,今天就安排一道具体题目。

0-1背包,每个物品只有一个
完全背包,每种物品无限个
多重背包,每种物品个数不一样

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

第一印象

直接看题解学一下 纯净背包问题

看完题解的思路

文字题解甚至看不进去的程度

看完视频我理解了,都11点多了我还在学习,太酬勤了。

例子, 背包容量为4

重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

1.dp数组:

dp[i物品][j背包空间]二维数组。

dp[i][j] 代表,任取[0, i]个物品,在背包空间j的时候,价值最大是多少。

背包问题,数组的含义挺晦涩难懂的,这里要谨记他的含义,就是这么回事。

在这里插入图片描述

2.递推公式:

物品只有两种状态,装包里和放外头。

对于第 i 个物品来说
如果给它放外头,包里物品还是原来那些,价值不变。 所以这种情况,dp[i][j] = dp[i - 1][j]. 因为物品没有放进去,容量是不变的,j 还是原来的 j。

如果给它装包里,那么肯定总价值要加上这个物品的价值 value[i]。
这时候我就产生疑问了,那我这个物品一定能放进去吗?
这就要注意了,因为我们假设物品状态就是装包里,说明他必须在里面。而且说明它没装之前,背包的容量是不算它自己的,也就是 j - weight[i]。那么 dp[i][j] = dp[i - 1][j - weight[i]] + value[i]

最后选择装包里和放外头最大的那种情况。

3.初始化:

这道题的初始化是有讲究的,不能随便初始化称0 或者 1.

我们好好看这个二维数组
在这里插入图片描述

每个位置都是由上面格子,或者左上方区域的某个格子推出来的。是不是很像机器人走步的那道题。 所以为了每个格子都有能算出来,第一行和第一列就要初始化。第一列肯定都是 0 ,但是第一行是有意义的,物品0 在背包容量是 0 1 2 3 4的时候,最大价值是多少。写一个for循环就可以了

4.遍历顺序:

两层for循环,背包和物品谁在外层内层都可以。我没在遍历顺序碰过钉子,我觉得按逻辑思维去想就可以,等会写的时候试试吧

实现中的困难

在kama网写,力扣没有这道题,kama网AC模式,我根本写不出最基础的样子,这里记录一下吧。

例子, 背包容量为4

重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

public class BagProblem {
	
}

dp数组

因为背包容量为 0,是有意义的,所以我们背包容量声明的时候要 + 1.
dp[][0]就是背包容量为0 ,满了。
dp[][bagSzie]就是背包容量是bagSize,空的。

//在[0, i]的物品里任选,最大的价值是 dp[i][j]
        int[][] dp = new int[weight.length][bagSize + 1];

初始化

也可以不初始化第一列,因为本来就是0

 //初始化
        //当背包容量j=0的时候,一个物品也装不进去,所以第一列全是0
        for (int i = 0; i < weight.length; i++) {
            dp[i][0] = 0;
        }
        //初始化第一个物品的情况
        for (int j = 0; j < dp[0].length; j++) {
            //物品超过背包容量了
            if (weight[0] > j) {
                dp[0][j] = 0;
            } else {
                dp[0][j] = value[0];
            }
        }

递推公式

理解思路之后,逻辑不难,实现就不难。

//递推公式 我选择先遍历物品再遍历背包
for (int i = 1; i < weight.length; i++) {
    for (int j = 1; j < dp[0].length; j++) {
       //如果这个东西 放不进去了
       if (weight[i] > j) {
           dp[i][j] = dp[i - 1][j];
       } else {
           //能放进去就比大小,看放还是不放更值钱
           int inBag = dp[i - 1][j - weight[i]] + value[i];
           int notInBag = dp[i - 1][j];
           dp[i][j] = Math.max(inBag, notInBag);
       }

    }
}

遍历顺序

大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!

但先遍历物品再遍历背包这个顺序更好理解。

其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了。

代码随想录里这么写的 ⬆️

感悟

我觉得如果只是理解这个递推公式我可以

但是为什么要这么递推呢?或者说,我一想到背包容量 j 在变化,我就不理解。为什么要看 j = 2的时候,第二件物品放进去和不放进去的的价值???

我再捋一捋

代码随想录里这么写的⬇️

讲了这么多才刚刚把二维dp的01背包讲完,这里大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在如何初始化和遍历顺序上。

可能有的同学并没有注意到初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。

下一篇
还是理论基础,我们再来讲一维dp数组实现的01背包(滚动数组),分析一下和二维有什么区别,在初始化和遍历顺序上又有什么差异,敬请期待!

代码

public class bagProblem {

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

    }

    public static void dynamic(int[] weight, int[] value, int bagSize) {
        //在[0, i]的物品里任选,最大的价值是 dp[i][j]
        int[][] dp = new int[weight.length][bagSize + 1];

        //初始化
        //当背包容量j=0的时候,一个物品也装不进去,所以第一列全是0
        for (int i = 0; i < weight.length; i++) {
            dp[i][0] = 0;
        }
        //初始化第一个物品的情况
        for (int j = 0; j < dp[0].length; j++) {
            //物品超过背包容量了
            if (weight[0] > j) {
                dp[0][j] = 0;
            } else {
                dp[0][j] = value[0];
            }
        }

        //递推公式 我选择先遍历物品再遍历背包
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j < dp[0].length; j++) {
                //如果这个东西 放不进去了
                if (weight[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //能放进去就比大小,看放还是不放更值钱
                    int inBag = dp[i - 1][j - weight[i]] + value[i];
                    int notInBag = dp[i - 1][j];
                    dp[i][j] = Math.max(inBag, notInBag);
                }

            }
        }

        // 打印dp数组
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < dp[0].length; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }

}

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

第一印象

直接看题解,怎么用一维dp数组就做出来

看完题解的思路

重量价值
物品0115
物品1320
物品2430

bagsize = 4

1. dp数组

在使用二维数组的时候,递推公式:
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](一维数组,也可以理解是一个滚动数组)。

所以,在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

2. 递推公式

上面提到了
dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

就是把 i 的维度去掉了。但仍然需要两层 for 循环

3. 初始化

dp[0] 一定是 0. 因为背包容量j为0的时候,什么也装不进去,价值自然是0.

那剩下的呢?我要看递推公式,每次用计算出的结果和自己相比较,取最大的。所以初始化的数字一定是 <= 0 的。而价值不能是负的,更符合实际意义,所以初始化成 0 就好了。

4.遍历顺序(我觉得最重要)

在二维的时候,我们正序遍历。

但在一维的时候,我们要倒序遍历。先给出结论

记住公式 dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

举上面的例子,
物品0在 j=1 的时候,dp[1] = max( dp[1], dp[0] + value[0] ) = max (0, 0+15) = 15. 合理

物品0在 j=2 的时候,dp[2] = max ( dp[2], dp[1] + value[0] ) = max (0, 30) = 30. 这就不合理了

这道题每个物品只能用一次
物品0大小是 1,那么背包容量j=2的时候,应该还是1个物品0的价值15。
但是正序遍历会让递推公式在计算价值的时候,把物品0的价值在 j=1 的时候和 j=2 的时候加在一起。

数值上看确实是不合理的,但是逻辑上呢?

我觉得是这样的,包现在是空的,我们把物品0 往里放,算包的容量如果是 1 2 3 4时候的最大价值。如果能放进去,就是这个物品不放包里时(也就是j - weight[i])包里的价值 + 这件物品的价值。

而这个不放包里时包里的价值应该是 上一件物品在这个 j 的最大价值。如果顺序遍历,比如 j=1 的时候是15,j = 2的时候是15 + 物品的价值15.

这里的第一个15代表容量为 1 时,物品0的最大价值。而不是什么也不放时的最大价值。

举个数量多的例子。
也就是对于重量为 1 的物品 4 来说,他在容量为j=5 的时候,计算的结果应该是,物品0~3 在 j=4 时的最大价值 + 物品4 的最大价值。而不是物品0~4在 j=4 时的最大价值 + 物品4 的价值。

对于物品 i, dp[j] 的数值是基于 0~ i-1 件物品在 j 和 j-weight[i] 的最大价值,也就是这个dp[j] 是用自己和滚动前它左侧的某个数算出来的,所以不能正序遍历,不然它就是基于滚动后左侧的某个数 和 自己算出来的了。就不对了。滚动后左侧的数会让它自己反复的加了多次,感到疑惑的话就拿题解里的例子画一画就行了

所以要倒序遍历背包容量 j。这样dp[j] 就可以基于滚动前 左侧数值求出了。

0-1背包的究极遍历和初始化结论,我觉得也是dp问题的一个经验。

上面是我从产生疑问 到 解决疑问的思路过程。
总之,我基于我和滚动前左边的数算出来的,那么求我之前,我左面的数不能求。所以倒序

我好像找到了规律,首先要画出来方便理解

1.机器人格子那道题,每个格子由上面和左边的格子求出来,所以初始化要第一行和第一列。而遍历的时候无所谓,一行一行,一列一列都可以。如图
在这里插入图片描述

2.二维dp的背包,每个数字由上面的和左上方的某个求出来,所以初始化的时候要第一行和第一列。而遍历的时候,也无所谓,每个数都是由上一行和左上方数求出来的。如图
在这里插入图片描述

3.一维dp的背包,每个数字基于自己和滚动前左面的某个(就是二维压缩后的结果)。所以初始化要控制自己很小,和初始化dp[0]。而遍历的时候,只能从后向前才能达到图里的样子。

在这里插入图片描述

实现中的困难

实现的时候没有什么困难。

for (int j = bagSize; j >= 1; j--) {
        if (j >= weight[i]) {
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        } else {
                    //第i个放不进去时,0~i个的最大价值和0~i-1的最大价值相同
                    //dp[j] = dp[j];
                }

 }

注意这里 j >= weight[i] 第一次写成 > 了

感悟

思考完这些,我就知道背包问题和之前做的动态规划相同的地方了。

之前做的动态规划, 1和2的时候一般很简单,3开始就是找规律了。会有那种数量关系或者推导关系,比如3是依靠12,4依靠23. 或者我依靠上面和左面的格子。或者我是通过比较两个数值最大的那个得来的,一个数值比较直观,一个数值是依靠我之前的哪个dp数组的数求来的。

他们的关系往往比较简单,体现在数字上。

而背包问题就不一样了。容量在变,物品在变。做2维数组dp背包的时候我想不出来这种逻辑关系。。。我不理解为什么要看不同容量 j 时候的最大值?包容量不变的啊。

做到1维的时候我就明白了,0~i个物品放进容量 j 的背包的最大值取决于,放包里时 0~i-1个物品放进容量 j - 第i个物品大小的背包的最大值,和搁外头时0~i-1 个物品放进容量 j 的背包的最大值。 总之就是0~i 和 0~i-1 有关系。由于 容量 j - 第i个物品大小的背包的最大值 是不确定的,要把 0~i-1 在每个 容量j 的最大值都求出来。这就是为什么要有不同的 容量 j 。

代码

public class bagProblem {

    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWight = 4;
        dynamic2(weight, value, bagWight);
    }

    // 二维dp数组解决 0-1 背包问题
    public static void dynamic(int[] weight, int[] value, int bagSize) {
        //在[0, i]的物品里任选,最大的价值是 dp[i][j]
        int[][] dp = new int[weight.length][bagSize + 1];

        //初始化
        //当背包容量j=0的时候,一个物品也装不进去,所以第一列全是0
        for (int i = 0; i < weight.length; i++) {
            dp[i][0] = 0;
        }
        //初始化第一个物品的情况
        for (int j = 0; j < dp[0].length; j++) {
            //物品超过背包容量了
            if (weight[0] > j) {
                dp[0][j] = 0;
            } else {
                dp[0][j] = value[0];
            }
        }

        //递推公式 我选择先遍历物品再遍历背包
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j < dp[0].length; j++) {
                //如果这个东西 放不进去了
                if (weight[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //能放进去就比大小,看放还是不放更值钱
                    int inBag = dp[i - 1][j - weight[i]] + value[i];
                    int notInBag = dp[i - 1][j];
                    dp[i][j] = Math.max(inBag, notInBag);
                }

            }
        }

        // 打印dp数组
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < dp[0].length; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }

    //一维dp数组解决 0-1 背包问题
    public static void dynamic2(int[] weight, int[] value, int bagSize) {
        int number = weight.length;
        //dp数组,大小是背包的大小 + 1
        int[] dp = new int[bagSize + 1];
        //初始化
        //都是0 省略了

        //递推公式
        for (int i = 0; i < number; i++) {
            for (int j = bagSize; j >= 1; j--) {
                if (j >= weight[i]) {
                    dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                } else {
                    //第i个放不进去时,0~i个的最大价值和0~i-1的最大价值相同
                    //dp[j] = dp[j];
                }

            }

        }

        for(int k = 0; k <= bagSize; k++) {
            System.out.println(dp[k]);
        }
    }
}

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

第一印象

我觉得得排序,排序了方便求和大小。还能剪枝优化。

但是我怎么看不出来和背包有啥关系呢。感觉回溯也能做?

我排序了,然后让 到第i个数的和是dp[i] 。初始化dp[0] = 0;

然后

for (int i = 1; i < dp.length; i++) {
            //到第 i 个数的总和是dp[i]
            dp[i] = dp[i - 1] + nums[i - 1];
            //剩下的数的和是rest
            int rest = totalSum - dp[i];
            if (rest == dp[i]) return true;
   }

炸一下想没毛病。但其实 1122这样的就不行了,12 = 12 排序了去看部分和,1 2 4 6 都不是总和6的一半,所以不行。

那换个思路,找几个数字,让他们的和是总和totalSum的一半呢?

还是没法往背包上靠,看题解了。
记录一下失败的思路代码

class Solution {
    public boolean canPartition(int[] nums) {
        //对原数组排序
        Arrays.sort(nums);
        //到第 i 个数的总和是dp[i]
        int[] dp = new int[nums.length + 1];
        dp[0] = 0;
        int totalSum = 0;

        for (int i = 0; i < nums.length; i++) {
            totalSum += nums[i];
        }

        for (int i = 1; i < dp.length; i++) {
            //到第 i 个数的总和是dp[i]
            dp[i] = dp[i - 1] + nums[i - 1];
            //剩下的数的和是rest
            int rest = totalSum - dp[i];
            if (rest == dp[i]) return true;
        }
        return false;
    }
}

看完题解的思路

就是我上面说的找几个数字,让他们的和是总和totalSum的一半

思路是对的

比如 1 5 5 11,就是找数字的和为11,数字相当于物品,11相当于背包容量。

问题变成能不能找到几个物品塞满背包,而之前的背包问题是每个物品还有价值,问往背包里放物品,最大价值是多少,不一定是塞满的。

所以我联系不上背包问题,因为我没找到两个数组。

这道题很巧妙啊,物品重量看做 1 5 5 11,价值也看做1 5 5 11.
拿物品放在包里,如果dp[11] = 11 就说明这个容量为11的背包被塞满了,而塞满他的物品的重量也正好是11.

诶还是有点绕,为什么要看最大价值啊?背包问题不是每次都求最大价值吗?

看题解学习一下思路吧。

dp数组

01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]
本题中每一个元素的数值既是重量,也是价值。

套到本题,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。

所以dp[target] = target 就代表放入物品的重量 等于 背包的容量。

这里容易困惑,背包还能装不满吗?想象打游戏时候,三级包确实可以装不满。

递推公式

物品i的重量是nums[i],其价值也是nums[i]。

所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

初始化

和背包是一样的

遍历顺序

和背包是一样的

但我还是不理解为什么要求最大的重量,但是两个都是重量我理解了。

那么到底为什么每次要看最大的属性B,最大的value,最大的重量呢?
之前的题因为题里说了要最大的value

而这道题,容量为 j 的包,最大的重量都不够 j(塞满背包),就不可能了啊。肯定不会超过的 j 的。所以去看最大的属性B 就行。

背包问题再次深入思考

背包问题就是,一堆东西有不同大小(大小属性A),我要往一个大小为size的包里装。

在动态规划算法的过程中,我能求出这些东西的另一个属性(B)的最大值,在这里我把另一个属性也当做这些东西的大小(也就是上面说的属性A)。

我能求出**dp[j] 含义是这些东西在包容量 j 时候的属性B的最大值。**在0-1背包里就是价值,在这里仍为大小。

而我最后希望容量 j 是size ,也就是背包的总容量,求这个情况时候的属性B最大值。但是dp[size] 依赖 dp[size - 1] ,依赖dp[size - 2] …… ,dp[j] , dp[j -1]……dp[0]。

在这个过程中,并不控制属性A:大小,一定是恰好塞满背包容量size,只是保证能装进去,且属性B的结果是最大的。

但是这道题属性AB是相同的,我想求背包是否被塞满,也就是看背包容量是满的时候 j = target,背包里的东西 dp[target] 有没有 target 这么多。

这里的有没有target这么多,用的是属性B,因为上面说了,dp[j] 含义是这些东西在包容量 j 时候的属性B的最大值。

那么属性A用在哪里呢?用来生成这个数组。但数组里面存的是属性B。这道题恰巧都是重量而已。

那么到底为什么每次要看最大的属性B,最大的value,最大的重量呢?
之前的题因为题里说了要最大的value

而这道题,容量为 j 的包,最大的重量都不够 j(塞满背包),就不可能了啊。肯定不会超过的 j 的。所以去看最大的属性B 就行。

实现中的困难

我先回家做饭,tmd牛羊肉我求你别下班啊

思路清晰之后就是套用背包。

感悟

我觉得把背包套用,还可以吧

但是理解逻辑,还是会磕巴

代码

class Solution {
    public boolean canPartition(int[] nums) {
        //数组求和
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
           sum += nums[i];
        }
        //如果和是奇数,分不出两个相等的
        if (sum % 2 != 0) return false;
        //算出选出的数的和应该是多少
        int target = sum / 2;
        //dp数组
        int[] dp = new int[target + 1];
        //初始化 都是0

        //递推公式
        for (int i = 0; i < nums.length; i++) {
            for (int j = target; j >= 1; j--) {
                if (j >= nums[i]) {
                    dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
                }    
            }
        }
        for (int i = 0; i < dp.length; i++) {
            System.out.println(dp[i]);
        }    
        return dp[target] == target;   
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值