代码随想录算法训练营第43天 || 1049. 最后一块石头的重量 II || 494. 目标和 || 474.一和零

代码随想录算法训练营第43天 || 1049. 最后一块石头的重量 II || 494. 目标和 || 474.一和零

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

题目介绍

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5

个人思路

本题与昨天写的分割相等子集有异曲同工之处,本题石头互相碰撞,我们不要陷入它的模拟状态,跳到上帝模式,我们先将石头分成两堆,想象成两块大石头相互碰撞,这样我们自然能想到要分成尽可能接近的两堆才能得到题解。所以这又回到了01背包问题上了,石头重量和价值都相同,背包容量设置成总重量的一半,最终看能放入的石头的最大价值即可。

动规五部曲

  1. 确定dp数组及其下标含义

    dp[j]表示背包容量为j的背包能放入石头的最大价值(for循环遍历石头限制0~i个石头可放入)

  2. 确定递推公式

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

    放与不放第i块石头的比较,前者是不放i石头的上一状态最大价值,后一状态是放i石头的最大价值

  3. 初始化dp数组

    因为价值都是大于0,所以我们一开始的默认初始化0即可,代码隐式初始化了

  4. 遍历顺序确定

    • 先遍历物品再遍历背包,反之没意义啊,递推公式直接作废
    • 背包的遍历要逆序,否则会出现前面的dp数组用新状态来参与计算(重复加入i石头)
  5. 打印dp数组检验

代码:

class Solution {
    public int lastStoneWeightII(int[] stones) {
        //本质上将石堆分成两份,是两份重量尽可能接近
        //然后我们发现这道题和昨天写的那道题分割等和子集差不多,这题就是尽可能分割等和子集
        //其实就是找到最接近stone总重量的一半
        //01背包:石头重量价值都一样,价值用于记录重量;背包容量设置总重量一般即可
        int sum = 0;
        for (int i = 0; i < stones.length; i++) {
            sum += stones[i];
        }
        int num = sum / 2;
        int[] dp = new int[num + 1];
        for (int i = 0; i < stones.length; i++) {
            for (int j = num; j > 0; j--) {
                if (j - stones[i] >= 0)
                    dp[j] = Integer.max(dp[j], dp[j - stones[i]] + stones[i]);
//                System.out.println(i + " " + j + " " + dp[j]);
            }
        }
        return sum - 2 * dp[num];
    }
}

494. 目标和

题目介绍

给你一个整数数组 nums 和一个整数 target

向数组中的每个整数前添加 '+''-' ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

个人思路及误区

之所以没能解出来,一个大问题就是第一步确定dp数组含义有误,误将本题和之前题相同处理,把dp[j]理解为数字的总和,每次达到这个总和就记录一次结果,结果绕来绕去感觉回到了小回溯的思路,不能解决本题。所以确定dp数组及其下标也是非常重要的。

题解解析

本题有点类似上一题的石头分类,这里是分成 加数 和 减数 两大类,然后运算后得到目标值。

由此,我们可以得到减数num = (sum - target)/ 2;(这里也可以用加数做运算)这样我们就可以将本题转化成求背包容量为num的01背包问题(每个物品只能放一次)

不过本题与之前的问题不一样,之前问的都是容量为j的背包最多能装多少?–>能不能装满? 本题问的是装满的有几种方法。因此,我们定义的dp数组的含义为:dp[j] 表示:容量 j 的背包有多少中方法装满

一些特殊情况的排除:(不排除可能会影响动规的结果,因为dp[0] = 1)

  1. target的绝对值 > sum ,这样是所有数值统一取﹢或﹣都不可能得到结果
  2. (sum - target) % 2 == 1这种情况也是无解的,我们知道sum可以分成两部分 left 和 right ,其中left - right = target,这也就意味着两堆数插值target,所以sum - target == 2 * right 必然为偶数

动规五部曲

  1. 确定dp数组及其下标含义

    dp[j] : 背包容量为 j 的背包最多有几种情况装满

    一个细节:由于target可以小于0,(num =(sum + target)/ 2 情况下)也就导致num可能小于0,我们要先取绝对值再new数组。

    这样没有影响,会对称回去。举个例子:

    sum = 100 ,target = -50 ,num = -75 (另一个num就是 25)num = 75其实就是25的对称值。

    当然,我们一开始设置 num =(sum - target)/ 2就不会产生负数情况,真要产生就是上面的不符情况1.

  2. 确定递推方法

    dp[j] = dp[j-nums[i]] 可以这么理解,还是先从放与不放引入。

    当遍历到第 i 个物品,背包容量为 j 时,要知道此时放入 i 情况的最多方法,其实就等价于容量为j - nums[i]的背包装满的最多方法

    注意:如果放入i比背包容量大,就不会刷新此刻的dp数组,保持原来的最大值(if判断)

  3. 初始化dp数组

    dp[0] = 1 这里不用过于解释为什么一开始容量为0的背包有一种情况装满,如果 = 0 递推公式一直都是0;

    这里可能会有疑问,万一数组 [0,0,0,0,0] target = 0怎么办?这样一开始设置为dp[0] = 1,会不会导致最终结果多1

    不用担心,1 — 2 — 4 — 8 — 16 — 32 五个过程走过来,+0和-0就是两种情况了,所以就算是[0] target = 0 也可以得到结果 2

    所以,这里的初始化也是很重要的

  4. 确定遍历顺序

    和之前一样,遵循两个规则

    • 先遍历物品后遍历背包
    • 背包层后序遍历
  5. 打印dp数组检验

代码:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++)
            sum += nums[i];
        //两种不符情况排除
        if (sum < target || sum < target * (-1))
            return 0;
        if ((sum - target) % 2 == 1)
            return 0;

        int num = (sum - target) / 2;
        //dp[j]装满容量为j的背包有多少种方法
        int[] dp = new int[num + 1];
        dp[0] = 1;//初始化
        for (int i = 0; i < nums.length; i++) {
            for (int j = num; j >= 0; j--) {
                if (j - nums[i] >= 0)
                    dp[j] += dp[j - nums[i]];
            }
        }
        return dp[num];
    }
}

474.一和零

题目介绍

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

个人思路

这道题问的是子集最多可以放几个元素,其中有两个限制条件。这与之前做的题目多了一个条件,这里背包容量确定上就有所区别,我们可以考虑用二维dp数组来确定1个背包2个分区容量。注意到这里每个元素只能放一次,所以这是一道01背包的变式。

动规五部曲

  1. 确定dp数组及其下标含义

    dp[i][j]表示能容纳i个0,j个1的背包最多能容纳几个物品

  2. 确定递推公式

    dp[j][k] = Integer.max(dp[j][k], dp[j - num_0][k - num_1] + 1);
    

    解释一下:本质还是放与不放的问题。

    • 放物品 i :找到容量刚好去除 i 的重量的背包最大放入数量,再加上1就是放物品 i 的最大数量,即dp[j - num_0][k - num_1] + 1
    • 不放物品 i :上一状态的dp[i][j]就是还没放入物品 i 的情况

    所以我们保留两者更大的就是此时遍历到物品 i 可放入的最大数量

  3. 初始化dp数组

    默认初始化全为0即可,无需再次初始化,相当于隐式初始化

  4. 确定遍历顺序

    三重for循环:最外层遍历物品,里面两层遍历背包的不同分区容量

    下列所说的左上角元素:dp[j - num_0][k - num_1] + 1

    • 最外层一定是遍历物品,内两层可以调换顺序,因为整体趋势都是右下角遍历到左上角,只会用到当前位置和上一状态的左上角元素
    • 内两层for一定是逆序遍历,否则用到的左上角就是更新过的,导致重复放入物品 i 的情况
  5. 打印dp数组检验

其实本题和此前做过的背包装最大价值和最多石头差不多,只不过本题是最大数量,区别在于前面加的是价值或石头重量/价值,本题加的是数量;另一个升级点,本题两个条件作为背包容量限制条件

小总结

  1. 装满背包有多少中方法?
  2. 能不能装满一个背包
  3. 尽量装满一个背包
  4. 二维01背包问题

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