完整教程:LeetCode算法日记 - Day 58: 目标和、数组总和
目录
1. 目标和
https://leetcode.cn/problems/target-sum/
给你一个非负整数数组 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
提示:
1 <= nums.length <= 200 <= nums[i] <= 10000 <= sum(nums[i]) <= 1000-1000 <= target <= 1000
1.1 题目解析
题目本质
对每个元素二选一(加号/减号),本质是二叉决策树上的全路径计数问题:从左到右依次决定符号,统计最终累加和等于 target 的路径条数。
常规解法
直接 DFS/回溯:在位置 pos 同时尝试 +nums[pos] 和 -nums[pos],直到用完所有元素,命中目标就计数。
问题分析
决策树有 2^n 条路径,时间复杂度 O(2^n),空间为递归深度 O(n)。在 n<=20 的题目规模下可接受,但存在大量重复子问题,若数据更大或多测试会显得慢。
思路转折
若要进一步稳定到多组数据,可改成等价的 0/1 背包计数(子集和),这里只点到为止。
1.2 解法
算法思想
• 定义递归 dfs(pos, curSum):处理到下标 pos,当前累加和为 curSum。
• 分支:进入 dfs(pos+1, curSum + nums[pos]) 与 dfs(pos+1, curSum - nums[pos])。
• 终止:当 pos==len,若 curSum==target,答案计数 +1。
• 最终答案为根结点到叶子的“命中”路径条数。
i)预处理并保存 len 与 target(代码里用 t)。
ii)从 pos=0, curSum=0 调用 dfs。
iii)在 dfs 中:
若 pos==len,判断是否命中,命中则 result++;返回。
否则递归两条分支:+nums[pos] 与 -nums[pos]。
iv)递归结束后返回全局 result。
易错点
递归基与返回:
pos==len时务必只判断一次并立即返回,避免重复计数。int安全性:本题和的范围 [−1000,1000][-1000,1000][−1000,1000],int足够;若拓展到大数,可考虑long。
1.3 代码实现
class Solution {
int result; // 记录命中 target 的表达式数量
int len; // 数组长度
int t; // 目标 target
public int findTargetSumWays(int[] nums, int target) {
this.t = target;
this.len = nums.length;
result = 0;
dfs(nums, 0, 0);
return result;
}
// 回溯:在位置 pos 选择 +nums[pos] 或 -nums[pos]
private void dfs(int[] nums, int pos, int curSum) {
if (pos == len) {
if (curSum == t) result++;
return;
}
dfs(nums, pos + 1, curSum + nums[pos]); // 选择 +
dfs(nums, pos + 1, curSum - nums[pos]); // 选择 -
}
}
复杂度分析
时间复杂度:最坏 O(2^n),每个元素都有加/减两种选择。
空间复杂度:递归栈深度 O(n)。
2. 数组总和
https://leetcode.cn/problems/combination-sum/description/
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
2.1 题目解析
题目本质
在“元素可重复取、组合无序”的前提下,寻找所有和为 target 的组合。这是一个组合型回溯问题:用起始下标 pos 控制只向右选,天然去重。
常规解法
直接回溯
从 pos 开始循环,选入 candidates[i] 后递归到下一层;因为允许重复使用,下一层仍从 i 开始;当累加和到达 target 即收集路径。
问题分析
朴素回溯会产生大量无效分支(当前和已超过 target 仍在探索),复杂度指数级,虽然题目保证解的数量 < 150,但无剪枝时仍浪费显著搜索。
思路转折
在有写法的框架内(ret/path/t + dfs(nums, pos, cur) 不变)做两点小优化即可显著提速:
i)排序 + 单行剪枝:先对 candidates 升序;在循环中一旦 cur + nums[i] > t,因后续更大,直接 break,整层提前结束。。
ii)起始下标去重:保持“下一层仍从 i 开始”(不是 i+1),既支持重复取数,又自然避免 [2,3,2] 这类排列重复。
2.2 解法
解法
算法思想
• 定义 dfs(pos, cur):在区间 [pos..n) 内继续选,使当前和为 cur。
• 若 cur == t:把 path 复制进 ret,返回一条解。
• 枚举 i 从 pos 到 n-1:
— 若 cur + nums[i] > t 且数组已升序,直接 break;
— 选入 nums[i],递归 dfs(i, cur + nums[i])(允许重复使用);回溯弹出。
i)将 candidates 升序排序。
ii)初始化全局 ret(答案集合)、path(当前路径)、t(目标)。
iii)从 dfs(0, 0) 启动回溯。
iv)在 dfs 中命中 cur == t 时收集并返回;否则循环枚举:做“超目标即 break”剪枝;选入→递归→回溯。
v)返回 ret。
易错点
剪枝一定配合排序:只有升序时 cur + nums[i] > t 才能用 break(不是 continue)。
重复使用与去重:允许重复使用元素,因此下一层起点是
i而不是i+1。命中即返回:cur == t 立即收集并返回,避免无用分支。
回溯弹栈:递归后必须 remove 最后加入的元素,防止污染其他分支。
2.3 代码实现
import java.util.*;
class Solution {
List> ret;
List path;
int t;
public List> combinationSum(int[] c, int _t) {
ret = new ArrayList<>();
path = new ArrayList<>();
t = _t;
Arrays.sort(c); // 升序:启用“超目标即剪枝”的 break
dfs(c, 0, 0);
return ret;
}
// 回溯:从下标 pos 开始选择,当前和为 cur
public void dfs(int[] nums, int pos, int cur) {
if (cur == t) { // 命中目标,收集一份解
ret.add(new ArrayList<>(path));
return;
}
for (int i = pos; i < nums.length; i++) {
int v = nums[i];
if (cur + v > t) break; // 关键剪枝:已排序,后面的更大,直接停止本层循环
path.add(v);
dfs(nums, i, cur + v); // 可重复使用同一元素,因此下一层仍从 i 开始
path.remove(path.size() - 1); // 回溯
}
}
}
复杂度分析
时间复杂度:由空间决定的指数级,但“排序 + break 剪枝”能大幅减少无效分支,在“解数 < 150”的约束下运行稳定。
空间复杂度:递归深度上界约为 target / min(candidates),路径与调用栈合计 O(深度)。

浙公网安备 33010602011771号