目标和——01背包系列

题意

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

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

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

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

提示:

  • 1 <= nums.length <= 20

  • 0 <= nums[i] <= 1000

  • 0 <= sum(nums[i]) <= 1000

  • -1000 <= target <= 1000

思路分析

对于给定的数组nums,每个元素添加+或-两种符号,再将它们串联起来构造成为一个表达式,使得表达式的最终计算结果target,要求得这样子不同的表达式的数目。本题看起来和前面的 分割等和子集最后一块石头的重量II 是类似的,分割等和子集 中是将给定的数组分成两个子集,最后一块石头的重量II 中是将给定的stone数组分成两堆,本题中也可将nums分两部分:添加+的部分 以及 添加-的部分。

数组的元素和、添加+的元素的和为 、添加-的元素的和为 ,则:

,可得: 。题目给定的数组为非负整数,则 都为非负整数。

那么可得目标和:,其中为添加+的部分,对加上 - 后表达式计算的结果为。对进行变化得: 都为常数,这两个量确定后也就确定了。

因此,考虑数组的全部元素,可以组成 的方案数与原题所求的不同表达式的数目是相同的。

为什么考虑数组 的全部元素,可以组成 的方案数与原题所求的不同表达式的数目是相同的?

整个表达式中,数组的元素要么是添加+号要么是添加-号,如果添加+号的元素确定了,那么添加-号的元素也就确定了,上面已经确定了同样也是可以确定的(详见下面有解释),那么就是 在给定的数组选出一些数用于添加+号,这些被选出的数的和已经确定了为 (或者在给定的数组选出一些数用于添加-号,这些被选出的数的和已经确定了为问组成之后的数的和为的不同的方案数有多少种(或和为的不同的方案数有多少种)。这里分出来的两种情况二选一去解决本题即可。

如果添加+或添加-的方案数确定了,原题所求的数目也就确定了,因为添加完+剩下的数就都是添加-,或添加完-剩下的数就都是添加+。

因此,考虑数组,可以组成的方案数 原题所求的不同表达式的数目是相同的。

是组成 都可以吗?

都可以的。

  • 若用 来表示 ,则式子为:,变换后得到:

  • 若用 来表示 ,则式子为:,变换后得到:

后面的步骤无论是使用 还是 都是可以的,只有一些细节处理上差别,这两种都会在后面的代码中体现并解释差别。

写了这么多,是为了把 为什么本题可以使用01背包解决 给写细一些。问题转换为01背包问题后,计算出的 就是背包容量,那么就是要从 数组中选出一些数组成 ,求有多少种不同的组法/方案数。

动态规划

本题用二维数组及一维数组两种实现。先使用二维数组利于理解,用一维数组用于节省内存空间。且使用作为01背包中背包的容量。

使用二维数组

确定dp数组含义

代表:考虑数组 中下标为 0~ i 的元素,组成 和为 j 的不同的方案数。其中 的取值范围为,这和数组的初始化相关。

注意本题 的含义与前面做过的题目不同,分割等和子集中是 最大的求和结果为 j 的情况下,考虑 数组中下标为 0~i 的元素,所组成的子集的元素之和最大为最后一块石头的重量II中是 在最大质量为 j 的情况下,考虑 数组中下标为0~i 的元素,所组成的一堆石头的质量之和最大为。这两题的 的含义都和给定数组元素的值直接相关,但本题的数组元素是 ,而方案数,不直接相关,这点需要特别注意,也因为这一区别,递推公式也会不同。

确定递推公式

既然是01背包,那在满足一定条件下就有 不选 两种选择。满足什么条件?

  • 如果 ,代表当前元素的值超过最大的元素和的限制值(即背包的容量),一定不能选取该,则递推公式为:;

  • 如果 ,那就有 不选 两种选择,若 不选 来组成 ,则递推公式为 ;若 来组成,则递推公式为:。因为是求 方案数 ,因此将这两者相加即这种情况下 的值:

因为 数组 这一行是作为起始条件的,真正开始遍历是从 开始遍历,因此 数组与 数组的下标间相差 1,当遍历到 时应当是在遍历 数组下标为 的元素。

综上,递推公式为:

dp[i][j] = dp[i - 1][j];
if (j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];

初始化dp数组

本题需要初始化 代表 不考虑数组元素的情况下,组成 0 的方案数为1。

为什么初始化为 1 ?

在确定dp数组含义中写到了:本题的方案数,一定要有一个来作为 "方案数的开头 1",这个开头一定不能是 0,这个的原因可以在纸上根据递推公式推导一下 数组就知道了:

如果 往后推导出的 就都为 0,结果也为0。另外,原因之二,因为组成0的方案数就是 不选 种。

其余 需要初始化吗?

如果默认值不为0则需要初始化为0。下, 表示:不考虑数组元素的情况下,组成 的方案数为0。举个例子,当 时,由于不考虑数组元素即不从数组中选取元素来组成 ,那么方案数就是0。

为什么 就不为0而是1呢?原因之一已经在上面解释过,因为后续的推导需要它为 1,原因之二,因为组成0的方案数就是 不选 这一种,而不从数组选择数却组成大于0的数是不可能的。

于是,最后数组的初始化如下:

dp[0][0] = 1;

遍历顺序

二维 数组的遍历顺序可以是 先遍历背包再遍历物品先遍历物品再遍历背包,即可以是 外层for循环遍历 内层for循环遍历 外层 for 循环遍历 内层for循环遍历 。因为 来源于 数组中的上一行左上的元素,两层循环都是从大到小遍历。

// 外层for循环遍历subSum  内层for循环遍历nums 
for (int j = 0; j <= subSum; j++) {
    for (int i = 1; i <= nums.length; i++) {
        dp[i][j] = dp[i - 1][j];
        if (j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
    }
}
// 外层 for 循环遍历nums  内层for循环遍历subSum
for (int i = 1; i <= nums.length; i++) {
    for (int j = 0; j <= subSum; j++) {
        dp[i][j] = dp[i - 1][j];
        if (j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
    }
}

遍历中取到了,因为数组与数组的下标相差1,这在写 数组初始化时有示意图。

举例推导dp数组

使用一维数组

为节省空间,可优化二维数组为一维数组。使用一维数组下, 数组的含义、递推公式、初始化都和二维数组相似。

dp数组的含义

代表:考虑数组 中下标为 0~ i 的元素,组成 和为 j 的不同的方案数。其中 的取值范围为

递推公式

由于使用一维数组,因此每次遍历 的过程都会覆盖原先的值。

  • 时,

  • 时,.

综上得出一维数组下的递推公式为:

if (j >= nums[i - 1]) dp[j] += dp[j - nums[i - 1]];

初始化dp数组

一维数组下 的初始化和二维数组是一样的,初始化

遍历顺序

一维数组和二维数组的遍历顺序是不太一样的,这点在写 0-1 背包理论篇(下)中已经写到。一维数组下,背包的遍历顺序为:先遍历数组再遍历,遍历是从大到小遍历的。

需要注意的一点是:不能取到 ,因为初始未开始遍历时一维的 数组即为初始状态,与二维数组的情况相同。

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

举例推导dp数组

完整Java代码实现

/***
  使用一维dp数组
  subSum作为背包容量
*/
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i : nums) sum += i;
        if (target > sum) return 0;//目标和比数组元素和都大 方案数为0
        if ((sum - target) % 2 != 0) return 0;//sum - target可被2整除才能在数组中取元素组成subSum
        int subSum = (sum - target) / 2;
        int[] dp = new int[subSum + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = subSum; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[subSum];
    }
}
/***
  addSum作为背包容量
*/
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i : nums) sum += i;
        //addSum作为背包容量要注意sum+target是否小于0
        if (target > sum || sum + target < 0) return 0;
        if ((sum + target) % 2 != 0) return 0;
        int addSum = (sum + target) / 2;
        int[] dp = new int[addSum + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = addSum; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[addSum];
    }
}
/***
  使用二维dp数组
*/
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i : nums) sum += i;
        if (target > sum || sum + target < 0) return 0;
        if ((sum + target) % 2 != 0) return 0;
        int leftSum = (sum + target) / 2;
        int[][] dp = new int[nums.length + 1][leftSum + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= nums.length; i++) {
            for (int j = 0; j <= leftSum; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= nums[i - 1]) dp[i][j] += dp[i - 1][j - nums[i - 1]];
            }
        }
        return dp[nums.length][leftSum];
    }
}

《代码随想录》刷题记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值