Files
leetcode-master/problems/0039.组合总和.md

481 lines
16 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20210924105952.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 39. 组合总和
[力扣题目链接](https://leetcode-cn.com/problems/combination-sum/)
给定一个无重复元素的数组 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]
]
# 思路
[B站视频讲解-组合总和](https://www.bilibili.com/video/BV1KT4y1M7HJ)
题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**然后看到下面提示1 <= candidates[i] <= 200我就放心了。
本题和[77.组合](https://programmercarl.com/0077.组合.html)[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)和区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。
本题搜索的过程抽象成树形结构如下:
![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png)
注意图中叶子节点的返回条件因为本题没有组合数量要求仅仅是总和的限制所以递归没有层数的限制只要选取的元素总和超过target就返回
而在[77.组合](https://programmercarl.com/0077.组合.html)和[216.组合总和III](https://programmercarl.com/0216.组合总和III.html) 中都可以知道要递归K层因为要取k个元素的组合。
## 回溯三部曲
* 递归函数参数
这里依然是定义两个全局变量二维数组result存放结果集数组path存放符合条件的结果。这两个变量可以作为函数参数传入
首先是题目中给出的参数集合candidates, 和目标值target。
此外我还定义了int型的sum变量来统计单一结果path里的总和其实这个sum也可以不用用target做相应的减法就可以了最后如何target==0就说明找到符合的结果了但为了代码逻辑清晰我依然用了sum。
**本题还需要startIndex来控制for循环的起始位置对于组合问题什么时候需要startIndex呢**
我举过例子如果是一个集合来求组合的话就需要startIndex例如[77.组合](https://programmercarl.com/0077.组合.html)[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)。
如果是多个集合取组合各个集合之间相互不影响那么就不用startIndex例如[17.电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html)
**注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍**
代码如下:
```CPP
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)
```
* 递归终止条件
在如下树形结构中:
![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png)
从叶子节点可以清晰看到终止只有两种情况sum大于target和sum等于target。
sum等于target的时候需要收集结果代码如下
```CPP
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
```
* 单层搜索的逻辑
单层for循环依然是从startIndex开始搜索candidates集合。
**注意本题和[77.组合](https://programmercarl.com/0077.组合.html)、[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)的一个区别是:本题元素为可重复选取的**
如何重复选取呢,看代码,注释部分:
```CPP
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 关键点:不用i+1了表示可以重复读取当前的数
sum -= candidates[i]; // 回溯
path.pop_back(); // 回溯
}
```
按照[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中给出的模板不难写出如下C++完整代码:
```CPP
// 版本一
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 不用i+1了表示可以重复读取当前的数
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtracking(candidates, target, 0, 0);
return result;
}
};
```
## 剪枝优化
在这个树形结构中:
![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png)
以及上面的版本一的代码大家可以看到对于sum已经大于target的情况其实是依然进入了下一层递归只是下一层递归结束判断的时候会判断sum > target的话就返回。
其实如果已经知道下一层的sum会大于target就没有必要进入下一层递归了。
那么可以在for循环的搜索范围上做做文章了。
**对总集合排序之后如果下一层的sum就是本层的 sum + candidates[i]已经大于target就可以结束本轮for循环的遍历**
如图:
![39.组合总和1](https://img-blog.csdnimg.cn/20201223170809182.png)
for循环剪枝代码如下
```
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
```
整体代码如下:(注意注释的部分)
```CPP
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.push_back(path);
return;
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0, 0);
return result;
}
};
```
# 总结
本题和我们之前讲过的[77.组合](https://programmercarl.com/0077.组合.html)、[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)有两点不同:
* 组合没有数量要求
* 元素可无限重复选取
针对这两个问题,我都做了详细的分析。
并且给出了对于组合问题什么时候用startIndex什么时候不用并用[17.电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html)做了对比。
最后还给出了本题的剪枝优化,这个优化如果是初学者的话并不容易想到。
**在求和问题中,排序之后加剪枝是常见的套路!**
可以看出我写的文章都会大量引用之前的文章,就是要不断作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。
# 其他语言版本
## Java
```Java
// 剪枝优化
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates); // 先进行排序
backtracking(res, new ArrayList<>(), candidates, target, 0, 0);
return res;
}
public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int sum, int idx) {
// 找到了数字和为 target 的组合
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = idx; i < candidates.length; i++) {
// 如果 sum + candidates[i] > target 就终止遍历
if (sum + candidates[i] > target) break;
path.add(candidates[i]);
backtracking(res, path, candidates, target, sum + candidates[i], i);
path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
}
}
}
```
## Python
**回溯**
```python3
class Solution:
def __init__(self):
self.path = []
self.paths = []
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
'''
因为本题没有组合数量限制所以只要元素总和大于target就算结束
'''
self.path.clear()
self.paths.clear()
self.backtracking(candidates, target, 0, 0)
return self.paths
def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:
# Base Case
if sum_ == target:
self.paths.append(self.path[:]) # 因为是shallow copy所以不能直接传入self.path
return
if sum_ > target:
return
# 单层递归逻辑
for i in range(start_index, len(candidates)):
sum_ += candidates[i]
self.path.append(candidates[i])
self.backtracking(candidates, target, sum_, i) # 因为无限制重复选取所以不是i-1
sum_ -= candidates[i] # 回溯
self.path.pop() # 回溯
```
**剪枝回溯**
```python3
class Solution:
def __init__(self):
self.path = []
self.paths = []
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
'''
因为本题没有组合数量限制所以只要元素总和大于target就算结束
'''
self.path.clear()
self.paths.clear()
# 为了剪枝需要提前进行排序
candidates.sort()
self.backtracking(candidates, target, 0, 0)
return self.paths
def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:
# Base Case
if sum_ == target:
self.paths.append(self.path[:]) # 因为是shallow copy所以不能直接传入self.path
return
# 单层递归逻辑
# 如果本层 sum + condidates[i] > target就提前结束遍历剪枝
for i in range(start_index, len(candidates)):
if sum_ + candidates[i] > target:
return
sum_ += candidates[i]
self.path.append(candidates[i])
self.backtracking(candidates, target, sum_, i) # 因为无限制重复选取所以不是i-1
sum_ -= candidates[i] # 回溯
self.path.pop() # 回溯
```
## Go
主要在于递归中传递下一个数字
```go
func combinationSum(candidates []int, target int) [][]int {
var trcak []int
var res [][]int
backtracking(0,0,target,candidates,trcak,&res)
return res
}
func backtracking(startIndex,sum,target int,candidates,trcak []int,res *[][]int){
//终止条件
if sum==target{
tmp:=make([]int,len(trcak))
copy(tmp,trcak)//拷贝
*res=append(*res,tmp)//放入结果集
return
}
if sum>target{return}
//回溯
for i:=startIndex;i<len(candidates);i++{
//更新路径集合和sum
trcak=append(trcak,candidates[i])
sum+=candidates[i]
//递归
backtracking(i,sum,target,candidates,trcak,res)
//回溯
trcak=trcak[:len(trcak)-1]
sum-=candidates[i]
}
}
```
## JavaScript
```js
var combinationSum = function(candidates, target) {
const res = [], path = [];
candidates.sort(); // 排序
backtracking(0, 0);
return res;
function backtracking(j, sum) {
if (sum > target) return;
if (sum === target) {
res.push(Array.from(path));
return;
}
for(let i = j; i < candidates.length; i++ ) {
const n = candidates[i];
if(n > target - sum) continue;
path.push(n);
sum += n;
backtracking(i, sum);
path.pop();
sum -= n;
}
}
};
```
## C
```c
int* path;
int pathTop;
int** ans;
int ansTop;
//记录每一个和等于target的path数组长度
int* length;
void backTracking(int target, int index, int* candidates, int candidatesSize, int sum) {
//若sum>=target就应该终止遍历
if(sum >= target) {
//若sum等于target将当前的组合放入ans数组中
if(sum == target) {
int* tempPath = (int*)malloc(sizeof(int) * pathTop);
int j;
for(j = 0; j < pathTop; j++) {
tempPath[j] = path[j];
}
ans[ansTop] = tempPath;
length[ansTop++] = pathTop;
}
return ;
}
int i;
for(i = index; i < candidatesSize; i++) {
//将当前数字大小加入sum
sum+=candidates[i];
path[pathTop++] = candidates[i];
backTracking(target, i, candidates, candidatesSize, sum);
sum-=candidates[i];
pathTop--;
}
}
int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){
//初始化变量
path = (int*)malloc(sizeof(int) * 50);
ans = (int**)malloc(sizeof(int*) * 200);
length = (int*)malloc(sizeof(int) * 200);
ansTop = pathTop = 0;
backTracking(target, 0, candidates, candidatesSize, 0);
//设置返回的数组大小
*returnSize = ansTop;
*returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
int i;
for(i = 0; i < ansTop; i++) {
(*returnColumnSizes)[i] = length[i];
}
return ans;
}
```
## Swift
```swift
func combinationSum(_ candidates: [Int], _ target: Int) -> [[Int]] {
var result = [[Int]]()
var path = [Int]()
func backtracking(sum: Int, startIndex: Int) {
// 终止条件
if sum == target {
result.append(path)
return
}
let end = candidates.count
guard startIndex < end else { return }
for i in startIndex ..< end {
let sum = sum + candidates[i] // 使用局部变量隐藏回溯
if sum > target { continue } // 剪枝
path.append(candidates[i]) // 处理
backtracking(sum: sum, startIndex: i) // i不用+1以重复访问
path.removeLast() // 回溯
}
}
backtracking(sum: 0, startIndex: 0)
return result
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>