Day35 力扣动态规划 : 1049. 最后一块石头的重量 II |494. 目标和 |474.一和零

Day35 力扣动态规划 : 1049. 最后一块石头的重量 II |494. 目标和 |474.一和零

  • 1049. 最后一块石头的重量 II
    • 第一印象
    • 看完题解的思路
      • 从题目看到背包
      • dp数组
      • 初始化
      • 递推公式
      • 遍历顺序
    • 实现中的苦难
    • 感悟
    • 代码
  • 494. 目标和
    • 第一印象
    • 看完题解的思路
      • 组合类背包问题
      • dp数组
      • 递推公式
        • 递推公式的究极疑问
          • 二维角度理解递推公式
          • 一维角度理解递推公式
      • 遍历顺序就不说了
    • 实现中的困难
    • 感悟
    • 代码
  • 474.一和零
    • 第一印象
    • 看完题解的思路
      • 回顾之前的背包
      • dp数组
      • 递推公式
      • 初始化
      • 遍历顺序
    • 实现中的困难
    • 感悟
    • 代码

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

本题就和 昨天的 416. 分割等和子集 很像了,可以尝试先自己思考做一做。
视频讲解:https://www.bilibili.com/video/BV14M411C7oV
https://programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html

第一印象

哎卧槽太难了。这次还要一次只选2个,我不懂啊。看题解吧

看完题解的思路

从题目看到背包

我觉得背包问题,透过题目看到背包就是最难的。

他说两块石头相撞,撞出一个差值。再拿两块装……这个过程感觉很复杂,而且和背包没关系

但是最后就是撞没或者撞得就剩一个。

其实就是把所有石头分成两堆,尽可能的平均的两堆。这样撞出来的结果就是最小值。

想到这我产生一个疑问: 比如总重23,分成11 和 12,但是石头一定能凑出11么?不一定啊,万一是8 和 15这样子的呢?为什么就是target = 11 去做背包问题啊?

再细想一下: 其实不是石头重量凑出11,而是准备一个总容量11的背包,看这个背包能装的石头(往里装是属性A)最大价值(在这道题就是属性B的重量)是多少。

因为最理想的情况就是容量11的包装重量11的石头,这样结果就是 1,就是最小的了。

这么想的话,就是很合理的背包问题了啊,就从题目看到背包了。

就可以写出以下代码了。

//求出所有石头的总重
        int totalWeight;
        for (int i = 0; i < stones.length; i++) {
            totalWeight += stones[i];
        }
        //题干的最小情况时,背包的大小target应该是总重一半,看这个包装的最大价值石头是多少
        int target = totalWeight / 2;

dp数组

和分割相等子集一样,背包容量是 j 的时候,dp[j] 代表 j容量的背包能装的石头的最大属性B价值(本题也是重量)。

//dp数组的大小应该是背包的容量 + 1
int[] dp = new int[target + 1];

初始化

和分割相等子集一样,dp[0] = 0,其余元素应该初始化称最小的非负数。其实也就不用初始化了。

递推公式

背包问题的递推公式是

dp[j] = max( dp[j], dp[ j - weight[I] ] + value[I] );

在本题种weight 和value 都是stones[] 。

dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

那么就可以写出下面的代码:

//递推公式
for (int i = 0; i < stones.length; i++) {
    //背包容量能装进这个石头,才需要计算,否则保持滚动前的数据就可以了
    for (int j = target; j >= stones[i]; j--) {
        dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
    }
}

其中内层 for 循环的终止条件写成 j >= stones[i]; 更方便。

不然就需要 写一个 if条件进行判断,而且 else 里还不用写东西。这样不够高雅了。

for (int j = target; j >= 1; j--) {
        if (j >= nums[i]) {
            dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        }    
}

遍历顺序

懒得说了

实现中的苦难

感悟

我越来越理解我的 属性A B问题了。

在分割子集的时候我觉得怎么看背包装不装满有点晦涩难懂,到这里看的是背包最多装多少,我一下就明白了。
能不能装满就是看最多呀,最多装多少,能不能装满

我太牛了。

但更难的难在,怎么看出是背包问题吧

代码

class Solution {
    public int lastStoneWeightII(int[] stones) {
        //求出所有石头的总重
        int totalWeight = 0;
        for (int i = 0; i < stones.length; i++) {
            totalWeight += stones[i];
        }
        //题干的最小情况时,背包的大小target应该是总重一半,看这个包装的最大价值石头是多少
        int target = totalWeight / 2;
        //dp数组的大小应该是背包的容量 + 1
        int[] dp = new int[target + 1];

        //递推公式
        for (int i = 0; i < stones.length; i++) {
            //背包容量能装进这个石头,才需要计算,否则保持滚动前的数据就可以了
            for (int j = target; j >= stones[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        
        return totalWeight - dp[target] - dp[target];
    }
}

494. 目标和

大家重点理解 递推公式:dp[j] += dp[j - nums[i]],这个公式后面的提问 我们还会用到。
视频讲解:https://www.bilibili.com/video/BV1o8411j73x
https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html

第一印象

乍一看挺组合的,哪里背包了啊???我想想

比如题里的例子 1 1 1 1 1 ,目标凑出 3. 所有数字的和是5,那么我的背包就是 5-3 = 2.

也就是说我只要找出哪些数字能凑出2,前面加负号就行。就是背包总容量是 2. 数字的重量和价值也是一样的。

但是这道题不再是,能不能凑出来?而是肯定能凑出来,有几种的问题。这我也不太明白了

看看题解吧。

看完题解的思路

我上面的思路是错的,我给sum当做正数部分了,当有两个1取负,负数部分 -2, 正数部分就是 3 了。 结果就是1 而不是目标 3了。

正确的应该是,正数部分up,负数部分down :

up + down = sum = 5
up - down = target = 3
那么up =( sum + target) / 2 = 4才对

也就是背包大小是这个 up,看哪些数字加起来能凑够这个 4。那如果不能整除呢?那么就没有这样的答案,返回0了。

组合类背包问题

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了。

dp数组

dp[j] 代表 装满容量为 j 的背包,有dp[j] 种方法。这里注意,是装满 j,而之前的事往 j 装,属性B 最大是 dp[j],是不一定装满的。

递推公式

dp[j] += dp[j - nums[i]]

怎么理解呢。

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

比如外层for循环我拿到的nums[i],我想知道j = 5 的时候,也就是背包大小为5,有多少种方法。

  • num[I] = 1 的话,那我就需要知道凑出 4 有多少种方法,因为拿到的是 1,这个元素是确定的。 也就是dp[4] 是多少,dp[5] 就是多少。
  • 拿到 2 的话,我就需要dp[3]
  • 拿到3, 需要dp[2]
  • 拿到4,需要dp[1]
  • 拿到5,需要dp[0]

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

递推公式的究极疑问

为啥啊??? 为什么要加起来啊?,我拿过来一个数字,这个数字是确定的啊,直接不就是dp[j - nums[i]] 吗,为什么要累加啊?

答案:

我可以从二维dp数组的角度去理解这个累加,但我不太理解一维数组,卡哥讲的那种累加。

二维角度理解递推公式

如图
Day35 力扣动态规划 : 1049. 最后一块石头的重量 II |494. 目标和 |474.一和零_第1张图片

去画这个二维数组的过程中就感受到累加的意义了。

比如放入第三个物品(第三个1,图里的1.3)时,想要装满容量为 2的背包(j = 2)有多少种方法?

我拿到了第三个物品,重量是1. 此时第三个物品有两个状态,放入背包和不放入背包。

如果放入背包,就类似上楼梯的问题(联想一下),这个物品已经确定下来了,剩下的背包空间是 1。 问题转换成了,用前两个物品来放,放满容量为 1 的背包有多少种可能? 也就是dp[2 - 1] = dp[1]

如果没有放入背包, 问题转换成了,用前两个物品来放,放满容量为 2 的背包有多少种可能? 也就是dp[2]

那么这三个物品放满容量为 2 的背包有多少种可能呢?就是
dp[2] = dp[2 - 1] + dp[2],dp[2] += dp[2 - 1]

也就是我图里写的,3个1凑2有多少种?2个1 凑1种 + 2个1凑2种

这个例子全都是 1 ,让人感觉有点不和谐。

一维角度理解递推公式
dp[j] += dp[j - nums[i]]

Day35 力扣动态规划 : 1049. 最后一块石头的重量 II |494. 目标和 |474.一和零_第2张图片

上面这两个图,我不理解为什么 5 = 4+……+0

而是在滚动更新的过程中,我拿到第i个数,装满容量为 j 的包。依赖于用第i个数的种数和不用它的种数之和,用它就是dp[j - nums[i]],不用就是dp[j]

对比一下最大价值那种,滚动更新的过程中,我拿到第i个物品,装进容量为j的包。包内的最大价值 依赖于 用它装的价值 和 不用他装的价值,更大的那个。所以是 max()。

遍历顺序就不说了

实现中的困难

要注意特殊情况,除了up没有被整除,还有一种情况不可以。

因为nums数组都是非负的,想在数组里凑出 up 这大的背包,那么up一定非负。

所以

//如果不能整除就找不到
if ((target + sum) % 2 != 0) return 0;
//sum 一定是正数,如果target绝对值大于了sum绝对值,不可能
if (Math.abs(target) > sum) return 0;
//背包大小 up, 在nums里找数组 凑出up,所以up一定得非负,因为数组元素非负,凑不出负数的。
int up = (target + sum) / 2;

感悟

哎卧槽真难啊,这个类似组合总和的题目,是背包里的组合问题,考虑多少种情况,常用到这个递推公式。

代码

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }

        //如果不能整除就找不到
        if ((target + sum) % 2 != 0) return 0;
        //sum 一定是正数,如果target绝对值大于了sum绝对值,不可能
        if (Math.abs(target) > sum) return 0;
        //背包大小 up, 在nums里找数组 凑出up,所以up一定得非负,因为数组元素非负,凑不出负数的。
        int up = (target + sum) / 2;
        
        int[] dp = new int[up + 1];
        dp[0] = 1;

        for (int i = 0; i < nums.length; i++) {
            for (int j = up; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }

        return dp[up];
    }
}

474.一和零

通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。
视频讲解:https://www.bilibili.com/video/BV1rW4y1x7ZQ
https://programmercarl.com/0474.%E4%B8%80%E5%92%8C%E9%9B%B6.html

第一印象

好的,我直接看题解粗略了解8

看完题解的思路

回顾之前的背包

  • 01背包基础:背包尽量装,装的价值最多多少
  • 分割相等子集:给一个背包,问能不能装满
  • 最后一个石头:背包尽量装,最多装多少
  • 目标和:给一个背包,问装满有多少种方法
  • 一和零: 给一个背包,有两个维度,尽量装,问有最多能装多少个物品

dp数组

这道题的背包是两个维度的,也就是有两个属性A,m和n。

所以需要一个二维数组dp[][]

含义是 容量为 i 个0, j 个 1的背包,最多有dp[i][j] 个物品

最终求dp[m][n]

递推公式

0-1 背包的时候:

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

现在维度上升了,那就是 dp[i - x][j - y] + 1, 为什么是 + 1呢。因为求的最多多少个物品,所以就 + 1就行了。

同样,装进这个物品和不装进物品的数量 取最大值、

就是

dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

初始化

和0-1背包一样,就是0

遍历顺序

还是倒序

实现中的困难

代码上不难

感悟

这道题就是纯粹0-1问题的 2维版本

代码

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        //  m是0的个数,n是1的个数
        int [][] dp = new int[m + 1][n + 1];

        for (int i = 0; i < strs.length; i++) {
            int numOfZero = 0, numbOfOne = 0;
            String str = strs[i];
            //统计这个物品的两个重量
            for (int k = 0; k < str.length(); k++) {
                if (str.charAt(k) == '0') {
                    numOfZero++;
                } else {
                    numbOfOne++;
                }
            }

            for (int j = m; j >= numOfZero; j--) {
                for (int k = n; k >= numbOfOne; k--) {
                     dp[j][k] = Math.max(dp[j - numOfZero][k - numbOfOne] + 1, dp[j][k]);
                }          
            }
        }
        return dp[m][n];
    }
}

你可能感兴趣的:(leetcode,动态规划,算法)