0806算法 - 动态规划04

1049. 最后一块石头的重量 II | 494. 目标和 | 474.一和零

Posted by ZA on August 6, 2024

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],这就是最优值。

思路

本题思路与昨日 416.分割等和子集 非常相似,每个石头的重量就等同于数组元素的值,因此同样适用weight[i]=value[i]stones[target]=target,只不过本题明确是可以存在差值的(即最终剩下那块石头的余重),那么此时不等于sum/2就不能直接判false

当我们找出一堆dp[target]的石头后,剩余石头重量就为(sum-dp[target]), 由于sum/2向下取整,因此必定有sum-dp[target] ≥ dp[target] 因此这两堆石头相撞后的剩余重量就为(sum-dp[target])-dp[target]

  1. dp数组及其下标含义:背包用容量j装了价值/重量d[j]的石头
  2. 递推公式:同理为dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
  3. 数组初始化:同理受限于最大石头总重,可定义为vector<int> dp(15001, 0);
  4. 遍历顺序:同理i从小到大,j从大到小

代码实现

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        vector<int> dp(15001,0);
        int sum=0;
        for(int i=0; i<stones.size(); i++){
            sum += stones[i];
        }
        int target = sum/2;


        for(int i=0; i<stones.size(); i++){
            for(int j=target; j>=stones[i]; j--){
                dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]);
            }
        }

        return sum - dp[target] - dp[target];
    }
};

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

思路

  • 分解该问题,数组元素最终会分为正负两堆,则必有 正-负=target
  • 此外数组元素的总和是已知且固定的,即正+负=sum
  • 联立上面两式消去负元素,可得正=(target+sum)/2
  • 此时问题就转化为了——只要在数组中找出一组和为的元素,就表示构造出了一个运算结果为target的表达式
  • 建模为01背包问题:能装满背包容量为的元素的所有方法

特别的是,本题不再求能不能装或装多少,而是装满有几种方法?这是一类特殊的组合问题。

  1. dp数组及其下标含义:填满容量为j的背包,有dp[j]种方法
  2. 递推公式容量j==正本质上还是一组数组元素和,因此它与nums[i]息息相关:以nums=[1,2,3,4,5]要填满dp[5]为例,如果已有一个元素1,那么就只需要求dp[5-1]=dp[4]即可,以此类推:
    1. 遍历每个nums[i],想要凑成容量j都有dp[j-nums[i]]种方法
    2. 因此总共有dp += dp[j-num[i]]种方法
  3. 数组初始化dp[0]=1
    1. 对于数组[0]的target=0,无论取正取负,元素0都只有一种方式成为target
  4. 遍历顺序:同理,外循环正序,内循环倒序
  5. 举例推导: 输入:nums: [1, 1, 1, 1, 1], target: 3,dp数组变化如图所示:

代码实现

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        int bagSize = (target + sum) / 2;

        if (abs(target) > sum) return 0; // 超过总和,此时没有方案
        if ((target + sum) % 2 == 1) return 0; // 奇数,此时没有方案

        vector<int> dp(bagSize + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};

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 。

思路

  1. dp数组及其下标含义
  2. 递推公式
  3. 数组初始化
  4. 遍历顺序
  5. 举例推导

代码实现

后记

  • 动态规划的基本问题,大多直接取dp数组的最终结果为输出,因此dp[i]的意义通常就是输出的意义,此时再代入去分析i的含义即可
  • dp[n]代表n的最终结果,但数组下标从0开始,因此dp[n]的实际存储位置为n+1,所以在定义数组时要考虑多定义一个长度。

参考