LeetCode算法日记 - Day 58: 目标和、数组总和
目录
1. 目标和
1.1 题目解析
1.2 解法
1.3 代码实现
2. 数组总和
2.1 题目解析
2.2 解法
2.3 代码实现
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 <= 20
0 <= nums[i] <= 1000
0 <= 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; // 目标 targetpublic 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<List<Integer>> ret;List<Integer> path;int t;public List<List<Integer>> combinationSum(int[] c, int _t) {ret = new ArrayList<>();path = new ArrayList<>();t = _t;Arrays.sort(c); // 升序:启用“超目标即剪枝”的 breakdfs(c, 0, 0);return ret;}// 回溯:从下标 pos 开始选择,当前和为 curpublic 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(深度)。