40.组合总和 II
40 组合总和 II
题目
给定一个候选人编号的集合 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次。
注意: 解集不能包含重复的组合。
示例 1:
输入: candidates =
[10,1,2,7,6,1,5], target =8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates =
[2,5,2,1,2], target =5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 1001 <= candidates[i] <= 501 <= target <= 30
思路
这一题很像三数之和的一般化。三数之和,可以通过双指针完成,但这里的一个有效解的长度是未知的,意味着并不能固定指针个数,而要动态增添指针。
第一眼的思路
我第一遍写,与前一道题相似,使用递归完成,基本思路是
1 | combinationSum(candidates, target) = |
在上述伪代码中,用 * 表示取两个二维数组的所有项的所有组合。如 [[1], [2]] * [[3]] = [[1,3], [2,3]],特殊地,
[[1]] * [] = [],[[1]] * [[]] = [[1]],这里,将 [] 看作这个运算的 0,将 [[]] 看作这个运算的单位1,可以帮助理解这个 * 运算符;
用 + 表示并集,此时 [] 为并集运算的单位1,任意数组与 [] 取并集仍然等于它自己。
则上述递归式表示对于给定的 candidates 和 target,组合总和就是以下两个解集的并集
- 不包括第一项的
candidates和target的组合总和的解集 - 单独包括第一项的二维数组
*不包括第一项的candidates和target - 第一项的组合总和的解集的运算结果
还是很晦涩……,以 candidates = [1,3] 和 target = 4 为例,简化方法名为 getCS:
1 | getCS([1,3], 4) |
上述的伪代码里,getCS([4], 0) 表示从 [4] 这个数组中取个 0,很显然取不到,但是根据题意可以清楚,当算式简化为取 0 的时候,说明前面拆解的各项已经等于 target,此项的结果不应该否认前面算式的结果,所以应该假设 getCS(x, 0) = [[]],这里的 x 可以是任意数组,只有这个式子等于 * 运算的单位1,才能保证不对前面的结果产生影响。
根据语意也可知,getCS([], y) 在 y ≠ 0 时,应该返回 [],表示无法组合,继而否定前面所有的 * 运算结果。
上述本是一个合理的运算过程,但是,这个算法过于 人类,对于组合数稍微增加的用例,其计算量会陡然增加,从而在运算时超时。极端的用例子是,[1,1,1,1,1,1,1...1] 共 40 个 1,计算对于 target = 30 的组合总和,肉眼看过去,解集中有且只有一个解,即取 30 个 1,但是代入上述的算法中,可以发现,算法算了无数次的 getCS([1,...,1], 30) 这类的值,毫无意义。所以不出意外地,这个算法超时了。
正解思路
之后我换用一种类似遍历树的思路,将一个值之后的其他所有值看作它的分叉,则将此题的 candidates 转换为了一个可以被广度遍历或者深度遍历的东西,当然,实际我并没有真的做这样的转换,因为没有必要。
具体做法是:
- 首先将
candidates做升序排序,方便后面循环取值 - 设置一个解集
ans,存储遍历找到的解 - 接着设置一个当前遍历路径的数组
current和 当前遍历路径的总和sum - 显然
current和sum总是会一起变动,所以,可以写两个方法统一操作数组的值进出 - 接下来使用一个
head指针,依次将candidates中的每一项看作是树的根节点,对每棵树做如下的循环操作- 将当前节点加入遍历数组
current中,将计算后的sum与target做判断- 若
sum大于target,则说明当前遍历数组不能满足题意,由于candidates是递增的,故当前项之后的所有项也不会满足题意,所以当前项的前一项也要替换为其下一项① - 若
sum小于target,则说明当前遍历路径可以继续,则对遍历指针进行递增操作即可。递增的后的指针有可能超出candidates的长度,此时需要将当前项与前一项一起删除,并取前一项的下一项继续遍历 - 若两者相等,则将当前遍历路径增加到解集中。然后本应将当前项替换为下一项进行继续的遍历,但是仍然由于
candidates是递增的,当前遍历数组如果恰好满足题意,则当前项之后的项替换当前项后的遍历数组,一定不满足题意,故直接删除当前项和前一项,取前一项的下一项继续遍历
- 若
- 当
head指针超过candidates的长度后,循环结束
- 将当前节点加入遍历数组
- 最后返回解集
①:这里的取下一项是指,遍历数组删除一项或多项后,取最后删除的元素在 candidates 中的下一个元素作为下一项。其中,与最后删除的元素值相等的元素应该直接跳过再取下一项,避免重复。因此,遍历数组 current 不仅存放元素,还有其对应的在 candidates 中的下标,即遍历数组 current 的大概格式为:
1 | const current: Array<{ |
如果在遍历数组删除元素时,遍历数组清空,则意味着以当前 head 指针指向的元素作为树的有效路径被遍历完,则应该将 head 递增,这也是上述算法第 5 步可以跳出循环的原因。
例子
仍然以 candidates = [1,3] 和 target = 4 为例,
遍历的第一次循环,head 指向 1,遍历路径的和也为 1,递增遍历指针,然后 3 加入遍历数组。
此时的遍历数组为
1 | current = [ |
遍历数组的和等于 target,解集增加解 [1,3]。
然后按算法,将当前项与前一项移除遍历数组,此时遍历数组清空,应取删除的最后一个元素 1 的下一项 3 重新加入遍历数组。
当然这次的循环是断然找不到解的,所以最后的解集为 [[1,3]]
解
1 | /** |
这道题也挺有意思,虽然题意与上一道组合总和题相似,但是因为运算超时的原因,逼着我不得不使用非递归的方法实现

Mosu is located on the shore of Mosu Lake, facing the vast Chu Sea, backed by the Yihan Mountains. Thousands of miles of Mosu Desert can not erode the Mosu Valley. Thus the Mosu Empire was established.


