〇、例题
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

通过本题,可以复习回溯算法 , 并引出动态规划
一、回溯算法
本题不难想到 ,对于数组中的每一个数字, 要么添加负号 , 要么添加正号 , 我们可以通过递归,枚举出每一种情况 ,并找到满足题意的情况。
回溯算法的大致框架:
0.明确原始问题和子问题
1.确定递归函数的入参和返回值
2.明确递归结束条件
3.写单层处理(即递归逻辑)
1.无返回值的回溯(显式回溯)
class Solution {
private int count = 0;
private int curSum = 0 ;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums , target , 0);
return count;
}
public void dfs(int[] nums , int target , int index){
if(index == nums.length){
if(curSum == target){
count ++;
return;
}
return;
}
curSum += nums[index];
dfs(nums , target , index + 1);
curSum -= nums[index];
curSum -= nums[index];
dfs(nums , target , index + 1);
curSum += nums[index];
}
}
这里使用了一个全局变量curSum , 用来记录某一条路径的数字之和 , 对于当前index元素,我们有两种符号 。那么我们先选加号,curSum更新为curSum + nums[index] , 问题缩小为在(index + 1)及之后的元素中选值,使得总和为target,于是递归到下一层,在这一次递归结束后,我们需要回复curSum!
因为我们接下来还要再选减号 , 若不回复 ,是无法回退到当前节点的curSum的。
2.有返回值的回溯(隐式回溯)
在上面的做法中 ,我们用到了两个全局变量 , count和 curSum
事实上, 我们也可以将其作为递归函数的参数传递。
递归三部曲如下:
0.明确原始问题和子问题
原始问题即:从数组的 所有元素 中,通过加减组合得到 target。
子问题是:从数组的 第 index 个元素开始,通过加减后续元素得到 target。
当index到达nums.length时,没有后续元素了,于是必须返回了。
1.递归函数的入参及返回值
入参: nums数组 , target , 递归到当前节点是的curSum , 记录方案数量 ,遍历数组的索引 index
返回值:从当前 index 和 curSum 出发,后续所有元素能组合出 target 的 有效路径数目
2.递归终止条件:
即:index = nums.length ,到达叶子节点,我们必须返回 “从当前 index 和 curSum 出发,后续所有元素能组合出 target 的 有效路径数目” , 而后续以及没有节点了 ,这条路径是否合法是由叶子节点时的curSUM所决定 ,要么是一条合法的 ,返回1 , 要么是不合法的 ,返回 0 。
底层逻辑:
- 计数逻辑:每个叶子节点代表一条完整的路径,有效路径返回
1,无效路径返回0。 - 逐层累加:递归过程中,每个节点的返回值是其所有子路径的解数之和,最终根节点的返回值就是全局解数。
3.单层处理及向下一层递归
对于当前节点 ,处理无非是选加号或者减号,
若选加号 , 从下一层的index和curSum出发 , 会有 若干种 有效路径数目
同理减号。
那么总数 就是 当前节点 选加号和减号之后 ,路径数目之和!
于是,代码如下
class Solution {
public int findTargetSumWays(int[] nums, int target) {
return bk(nums , target , 0 , 0);
}
public int bk(int[] nums , int target , int index , int curSum){
if(index == nums.length){
if(target == curSum){
return 1;
}
else{
return 0;
}
}
int add = bk(nums , target , index + 1 , curSum + nums[index]);
int sub = bk(nums , target , index + 1 , curSum - nums[index]);
return add + sub;
}
}
3.小结
要把递归想象成一棵树 , 递归终止条件就是树的叶子节点
二、回溯+记忆化
上述的回溯算法,是没有任何剪枝的 ,往往只适用于枚举总数小于 20 的情况。
而对于某些回溯题 ,观察不同路径是否可能到达相同的 index 和中间状态(如 curSum)。
若能,则可以考虑记忆化 ,避免重复递归搜索。
本题中,

或者画一画递归树,发现不同路径中会有相同的index和curSum出现,于是可以使用一个缓存Map,每次递归之前,先在这个map中找,是否已经计算过了,若已经计算过,直接return,反之才递归搜索下一层 , 并且递归后,缓存一下curSum。
class Solution {
private HashMap<String , Integer> memo = new HashMap<>();
public int findTargetSumWays(int[] nums, int target) {
return dfs(nums , target , 0 , 0);
}
public int dfs(int[] nums , int target , int index , int curSum){
if(index == nums.length){
if(curSum == target){
return 1;
}else{
return 0;
}
}
String key = index + "," + curSum;
if(memo.containsKey(key)){
return memo.get(key);
}
int add = dfs(nums , target , index + 1 , curSum + nums[index]);
int sub = dfs(nums , target , index + 1 , curSum - nums[index]);
memo.put(key , add + sub);
return add + sub;
}
}
注意, 缓存键的设计要满足唯一性!
三、动态规划
1.二维dp
//动态规划 二维数组版
//dp[i][j]的含义: 从 0-i种物品中选 , 恰好填满容量为j的背包共有dp[i][j] 种方案
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0 ;
for(int num : nums){
sum += num;
}
if((sum + target ) % 2 != 0 || sum < Math.abs(target)) return 0;
int bagSize = (sum + target) / 2;
int m = nums.length;
int[][] dp = new int[m][bagSize + 1];
//初始化第一行 、 第一列
//1.第一行 , if判断是为了避免数组越界
//对于第一行 , 物品始终只有nums[0] , 背包容量从0 到 bagsize
//当且仅当bagSize大于等于nums[0]时 , 在遍历过程中能取得到 j = nums[0],
//那么此时 能填满容量为j的方案就是一种 , 就是取nums[0]这个物品 ,
//反之 , nums[0]直接就大于 bagSize , 恰好填满的方案为 0.
if(nums[0] <= bagSize){
dp[0][nums[0]] = 1;
}
//2.初始化第一列 , 物品从0 - n , 背包容量为 0
//很显然 , 对于背包容量为0的情况 , 物品重量只要不为0 , 方案数就为1(只能不选)
//而只要 第i个物品重量为0 , 第i个物品就有2种选择 ; 若后面又遇到了0 ; 方案数等于这一列上的0的个数的2次方
//假如:物品重量为 1 0 0 2 3 4 ,索引从0-5 , dp[0][0] = 1 ,dp[0][1] = 2
//dp[0][2] = 1 * 2*2(两个零都是可选可不选) , dp[0][3] = 1*2*2*1 (乘法原理) 以此类推
//综上可见, 第一列的初始化跟第一列物品中重量为0的个数有关
int zeroCount = 0;
for(int i = 0 ; i < m ; i ++){
if(nums[i] == 0) zeroCount ++;
if(zeroCount == 0){
dp[i][0] = 1;
}else{
int temp = 1;
for(int cnt = 0 ; cnt < zeroCount ; cnt ++){
temp *= 2;
}
dp[i][0] = temp;
}
}
//递推
for(int i = 1 ; i < m ; i ++){
for(int j = 1 ; j < bagSize + 1 ; j ++){
if(nums[i] > j ){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
}
}
}
return dp[m-1][bagSize];
}
}
2.一维dp
//动态规划 一维数组版
//dp[j]的含义 容量为j的背包 , 要装满有dp[j]种方法
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num : nums){
sum += num;
}
if((sum + target) % 2 != 0 || Math.abs(target) > sum){
return 0;
}
int postive = (sum + target) / 2;
int[] dp = new int[postive + 1];
dp[0] = 1;
for(int i = 0 ; i < nums.length ; i ++){
for(int j = postive ; j >= nums[i] ; j --){
dp[j] += dp[j-nums[i]];
}
}
return dp[postive];
}
}
4471

被折叠的 条评论
为什么被折叠?



