组合总和
题目链接: https://leetcode.cn/problems/combination-sum/
# LeetCode 39. 组合总和
# 题目描述
给你一个无重复元素的整数数组candidates
和一个目标整数target
,找出candidates
中可以使数字和为目标数target
的所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。
candidates
中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为target
的不同组合数少于150
个。
举个例子:
输入: candidates = [2,3,6,7], target = 7
输出: [[2,2,3],[7]]
解释:
2和3可以形成一组候选,2 + 2 + 3 = 7。注意2可以使用多次。
7也是一个候选,7 = 7。
仅有这两种组合。
# 知识回顾
回溯是一种通过将原问题分解为子问题来求解复杂问题的算法思想。它通常用于求解组合问题、排列问题等。回溯的核心思想是通过自上而下递归的方式枚举所有可能的解,直到找到符合条件的解为止。回溯算法通常使用递归函数来实现,每次递归时都会对当前状态进行判断,如果符合条件则继续递归,否则回溯到上一层状态。
# 思路解析
本题是组合问题需要枚举所有可能的解,可以用回溯法来解,回溯的关键在于划分子问题和确定回溯条件。
下图是target = 7
, candidates = [2, 3, 7]
,自上而下枚举所有可能解的过程,叶子节点为0
,说明找到一个符合条件的解,其解就是由根节点到叶子节点之间树枝上的数组成。比如[2, 2, 3]
,[2, 3, 2]
,[3, 2, 2]
,[7]
都是符合条件的解。
从上面的例子可以看出对于target = 7
的情况可以划分为求target = 5
,target = 4
,target = 0
三个子问题,这样就明确了子问题的划分: 对于target = n
,我们可以划分成target = n - candidates[0]
,...
,target = n - candidates[m-1]
,m
个子问题,其中m
为candidates
的长度。
接下来就是确定回溯条件,很明显本题的回溯条件是叶子节点为0
或为负数。
到这里回溯的两个关键问题都已经明确了,细心的同学应该已经看到对于上面的例子推导出了重复的解,[2, 2, 3]
,[2, 3, 2]
,[3, 2, 2]
这三个解是重复的,这个时候就需要剪枝来剪掉重复的解。要如何剪枝呢,其实只需要在推导值为n - candidates[i]
节点的子节点时,令target = n - candidates[i] - candidates[j]
,i <= j < m
就可以将重复的解给剪掉。
有些同学对剪枝这部分有点疑惑,还是用上面的例子,target = 7
,零钱的集合为candidates=[2, 3, 7]
。我们可以选择兑换三个2
、两个2
、一个2
、零个2
,四种情况。每种情况我们都选择把要兑换的2
一次性换完,然后再从除去2
以后的零钱集合[3, 7]
去兑换剩余的钱。这样就避免了重复解,如果还是有疑惑可以在纸上画一画,可以更直观的去理解。
# C++代码
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> temp;
help(candidates, target, 0, res, temp);
return res;
}
void help(vector<int>& candidates, int target, int start, vector<vector<int>>& res, vector<int>& temp) {
//回溯条件
if (target == 0) {
//找到符合条件的解,保存起来
res.push_back(temp);
//开始回溯
return;
}
//回溯条件
if (target < 0) {
//开始回溯
return;
}
int candidates_len = candidates.size();
//i=start是剪枝操作,如果i=0会存在重复解。
for (int i = start; i < candidates_len; ++i) {
if (target - candidates[i] >= 0) {
temp.push_back(candidates[i]);
//子问题
help(candidates, target - candidates[i], i, res, temp);
//回到上一个状态
temp.pop_back();
}
}
}
};
# 复杂度分析
时间复杂度: 时间复杂度为整个推导树节点的个数,最大为 1 + m + m2 + ... + mk-1,其中m
为candidates
的长度,k
为树的高度。
空间复杂度: 空间复杂度跟解的个数有关,为所有解的元素个数之和。