1. 题目
读题
https://leetcode.cn/problems/target-sum/description/
考察点
这道题的考察点是如何将一个看似复杂的问题转化为一个简单的问题,以及如何运用不同的算法思想来解决问题。具体来说,有以下几个方面:
- 如何利用数学公式将原问题转化为一个01背包问题,即如何找到背包容量和物品价值的对应关系。
- 如何使用深度优先搜索(DFS)来遍历所有可能的表达式,以及如何剪枝和优化搜索过程。
- 如何使用动态规划(DP)来求解01背包问题,以及如何优化空间复杂度和时间复杂度。
- 如何根据题目的条件和限制,排除一些不可能的情况,提前返回结果,减少不必要的计算。
2. 解法
思路
思路是这样的:
首先,我们要把原问题转化为一个01背包问题。原问题是给定一个整数数组nums和一个整数target,要求用’+‘或’-'号给数组中的每个元素添加符号,然后拼接成一个表达式,使得表达式的值等于target,返回有多少种不同的表达式满足这个条件。
我们可以把数组中的元素分为两类,一类是需要加号的,一类是需要减号的。假设需要加号的元素的和为x,需要减号的元素的和为y,那么有x-y=target,同时x+y=sum(nums),可以得到y=(sum(nums)-target)/2。因此问题就变成了,在数组中找到若干个元素,使得它们的和等于y。这就是一个01背包问题,每个元素可以选择放入或不放入背包,背包容量为y,目标是找到所有可能的放法。
然后,我们要定义一个一维数组dp,用来存储不同的背包容量对应的方案数。dp[j]表示当背包容量为j时,有多少种不同的方法可以从数组中选取若干个元素,使得它们的和等于j。初始时,dp[0]为1,表示当背包容量为0时,只有一种方法,就是什么都不选。
接下来,我们要遍历数组中的每个元素nums[i],并更新dp数组。对于每个元素nums[i],我们从后往前遍历dp数组,如果当前的背包容量j大于等于nums[i],就说明可以选择这个元素放入背包,那么就有dp[j] += dp[j-nums[i]],表示当前的方案数等于不选这个元素的方案数加上选这个元素的方案数。如果当前的背包容量j小于nums[i],就说明无法选择这个元素放入背包,那么就保持dp[j]不变。
dp数组的含义
dp数组是一个一维数组,用来存储不同的背包容量对应的方案数。dp[j]表示当背包容量为j时,有多少种不同的方法可以从数组中选取若干个元素,使得它们的和等于j。初始时,dp[0]为1,表示当背包容量为0时,只有一种方法,就是什么都不选。然后对于每个元素nums[i],我们从后往前遍历dp数组,如果当前的背包容量j大于等于nums[i],就说明可以选择这个元素放入背包,那么就有dp[j] += dp[j-nums[i]],表示当前的方案数等于不选这个元素的方案数加上选这个元素的方案数。最后返回dp[neg]就是最终的答案。
状态转移公式是:
- if( j >= nums[i] ) dp[j] = dp[j] + dp[j-nums[i]],
- if( j < nums[i] ) dp[j] = dp[j],
其中,dp[j]表示当背包容量为j时,有多少种不同的方法可以从数组中选取若干个元素,使得它们的和等于j。
nums[i]表示数组中的第i个元素。
状态转移公式的含义是,
对于每个元素nums[i],我们从后往前遍历dp数组,
如果当前的背包容量j大于等于nums[i],就说明可以选择这个元素放入背包,那么就有dp[j] += dp[j-nums[i]],
表示当前的方案数等于不选这个元素的方案数加上选这个元素的方案数。如果当前的背包容量j小于nums[i],就说明无法选择这个元素放入背包,那么就保持dp[j]不变。
代码逻辑
代码的逻辑可以分为以下几个步骤:
- 计算数组的总和sum,以及目标和target与总和的差diff。
- 判断diff是否为非负偶数,如果不是,直接返回0,因为无法找到满足条件的表达式。
- 计算需要减去的元素的和neg,即diff/2,这就是背包问题的容量。
- 定义一个一维数组dp,用来存储不同的背包容量对应的方案数。初始化dp[0]为1,表示当背包容量为0时,只有一种方法,就是什么都不选。
- 遍历数组中的每个元素nums[i],并更新dp数组。从后往前遍历dp数组,如果当前的背包容量j大于等于nums[i],就说明可以选择这个元素放入背包,那么就有dp[j] += dp[j-nums[i]],表示当前的方案数等于不选这个元素的方案数加上选这个元素的方案数。如果当前的背包容量j小于nums[i],就说明无法选择这个元素放入背包,那么就保持dp[j]不变。
- 返回dp[neg]作为最终的答案。
具体实现
public class TargetSum { public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int num : nums) { sum += num; } int diff = (sum - target); if (diff < 0 || diff % 2 != 0) { return 0; } int neg = diff / 2; int[] dp = new int[neg + 1]; dp[0] = 1; for (int i = 1; i <= nums.length; i++) { for (int j = neg; j <= nums[i-1]; j--) { if (j >= nums[i-1]) { dp[j] = dp[j] + dp[j - nums[i - 1]]; } } } return dp[neg]; } }
3. 总结
DFS和DP是两种常用的算法思想,它们的基本概念如下:
- DFS是一种搜索算法,它的原理是从一个起始节点开始,沿着一条路径不断向下探索,直到达到一个终止条件或者无法继续前进为止,然后回溯到上一个节点,再沿着另一条路径继续探索,直到遍历完所有可能的路径为止。DFS可以用递归或者栈来实现,它的优点是可以找到所有可能的解,缺点是可能会重复搜索或者陷入死循环。
- DP是一种优化算法,它的原理是将一个复杂的问题分解为若干个子问题,然后从最简单的子问题开始,逐步求解,将每个子问题的最优解存储起来,最后利用这些子问题的最优解来构造原问题的最优解。DP可以用数组或者哈希表来存储子问题的最优解,它的优点是可以避免重复计算和减少时间复杂度,缺点是可能会增加空间复杂度或者难以找到状态转移方程。