1. 今日语录
只有勇敢迈出新的一步,才能避免永无止境的递归。
2. 题目
组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
3. 思路
/**
* 看到题目,我总是会想到高中的排列组合题。
* 这类题目 其实很相似,本质都是 根据问题生成一棵树,终止的条件在题目里。
* 算法的优化无非就是对这棵树进行剪枝,尽可能避免这棵树的野蛮生长,通过减少递归次数来减少时间复杂度。
*
* 解法1 搜索回溯
*
* 对于这种回溯类的题目,我们基本上都会用到递归,
* 而递归的终止条件往往就是解法的关键所在。
*
* 在本题中,搜索组合的过程就是一棵树,candidates数组确定这棵树接下来的走向,
*
* idx 代表 已经采用的组合 = candidates[0,idx)
*
* 初始节点 = 0,当 累计值sum >= target时,递归结束。
* 当 idx === candidates.length,组合耗尽,递归结束。
*
* 因为 candidates的每一项可以 无限制重复被选取 ,所以这棵树接下来的走向就可以分为两类。
*
* 一类是 画饼派,跳过当前选项,除了idx += 1,什么也没做。
* 哈哈哈,其实是idx += 1后,交给下一层的做事派完成了。
*
* 一类是 做事派,选择当前选项,开始累加选项值
* */
var combinationSum_0 = function (candidates, target) {
let res = []
const dfs = (sum, path, idx) => {
if (sum === target) {
res.push(Array.from(path))
return
}
if (sum > target) {
return
}
if (idx === candidates.length) {
return
}
dfs(sum, path, idx + 1)
dfs(sum + candidates[idx], [...path, candidates[idx]], idx)
}
dfs(0, [], 0)
return res
}
/**
* 解法2 剪枝优化
* 在搜索回溯的过程中,sum > target的情况可能会比较多,
* 这就会导致过多不必要的递归。
*
* 如果我们在组合前对数组进行升序排列,前一个较小数字已经超了的话,
* 后面自然就不需要再尝试了,这样可以减少递归次数,减少时间复杂度。
*
*/
function combinationSum_1(candidates, target) {
const res = [];
const path = [];
candidates.sort((a, b) => a - b);
const backtracking = (j, sum) => {
if (sum === target) {
res.push(Array.from(path));
return;
}
for (let i = j; i < candidates.length; i++) {
if (sum + candidates[i] > target) break;
path.push(candidates[i]);
sum += candidates[i];
backtracking(i, sum);
path.pop();
sum -= candidates[i];
}
}
backtracking(0, 0);
return res;
}
(function () {
const candidates = [8, 2, 3, 6, 7];
const target = 7;
console.log('组合总和(搜索回溯)', combinationSum_0(candidates, target));
console.log('组合总和(剪枝优化)', combinationSum_1(candidates, target));
})()
4. 关键字
搜索回溯、剪枝优化