前言
从候选集中选取符合条件的结果集是我们经常遇到的一类开发问题。我们先假设:
- 候选集是
Candidates(C1, C2, C3, ..., Cn) - 结果集是
Answer(A1, A2, A3, ..., Am)那么对于组合问题,我们一般的选取思路: - 选择放入:
Answer中的每个位置Ai都可以选取Candidates中任意一个Cj放入。因此,每一个Ai的确定过程,就是遍历Candidates选择放入/不放入Answer的过程。 - 校验条件:每加入一个
Ai我们就校验一下当前结果集是否符合要求;符合则中止选择放入 的过程;否则继续放入或者提前中止。 - 剪枝优化:这里通常是指选择放入的候选集可以根据某些特定的条件跳过以减少迭代递归的过程。
一般的编程范式可以概括为:
function combine(candidates) {
ans = [];
dfs([]);
return ans;
function dfs(result) {
if (符合条件(result)) {
ans.push(result);
return;
}
for (let candidate of candidates) {
result.push(candidate);
dfs(result);
result.poo(candidate);
}
}
}
今天的刷题打卡,主要选取了 LeetCode 39. 组合总和、LeetCode 40. 组合总和 II、LeetCode 216. 组合总和 III 三道题目进行强化训练,体会同类组合题目的解题思路与其中细微的条件区别。
LeetCode 39. 组合总和
题目描述
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
思路
本题的题眼是无重复元素和无限制重复被选取。因此,本题的剪枝优化就是如何在重复选取的条件下不选出重复的组合。
我们知道(Ci, Cj) 和 (Cj, Ci) 其实是对称的选取过程,因此只要在 ans(Ci, Cj, Ck, ..., Cn) 中 i <= j <= k <= ... n,也就是 candidates 的下标始终保持递增(不走回头路),我们可以保证不选出重复的组合。
题解
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum = function (candidates, target) {
let ans = [];
dfs([], 0, 0);
return ans;
function dfs(nums, sum, next) {
if (sum > target) return;
if (sum === target) {
ans.push(nums);
return;
}
for (let i = next; i < candidates.length; i++) {
// 关键:可以重复选取,因此往下一层传递 i
dfs([...nums, candidates[i]], sum + candidates[i], i);
}
}
};
LeetCode 40. 组合总和 II
题目描述
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
- 所有数字(包括目标数)都是正整数。
- 解集不能包含重复的组合。
示例1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
思路
本题的题眼是每个数字(候选)在每个组合中只能使用一次。这道题相比 39 题的不同之处在于,candidates 中可能包含重复的数字而且在结果集中每个候选只能选择一次。
题解
/**
* @param {number[]} candidates
* @param {number} target
* @return {number[][]}
*/
var combinationSum2 = function (candidates, target) {
let ans = [];
candidates = candidates.sort((a, b) => a - b);
dfs([], 0, 0);
return ans;
function dfs(result, sum, next) {
if (sum > target) {
return;
}
if (sum === target) {
ans.push(result);
return;
}
for (let i = next; i < candidates.length; i++) {
// 跳过重复数字,避免重复的组合
if (i > next && candidates[i] === candidates[i - 1]) continue;
// 关键:不可以重复选取,因此往下一层传递 i + 1
dfs([...result, candidates[i]], sum + candidates[i], i + 1);
}
}
};
LeetCode 216. 组合总和 III
题目描述
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数
- 解集不能包含重复的组合
示例1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
思路
本题题眼是每种组合中不存在重复的数字,这 40 题一脉相承,只是多了限制组合可选取的数量。
题解
/**
* @param {number} k
* @param {number} n
* @return {number[][]}
*/
var combinationSum3 = function (k, n) {
let ans = [];
dfs([], 0, 1);
return ans;
function dfs(result, sum, next) {
if (result.length > k || sum > n) {
return;
}
if (sum === n) {
if (result.length === k) {
ans.push(result);
}
return;
}
for (let i = next; i <= 9; i++) {
dfs([...result, i], sum + i, i + 1);
}
}
};
总结
又来打卡啦~本文主要基于深度递归/回溯剪枝的思想,处理这类简单的组合问题。