Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commita15a13e

Browse files
Update
1 parent19652ea commita15a13e

File tree

3 files changed

+166
-43
lines changed

3 files changed

+166
-43
lines changed

‎pics/39.组合总和.png

12.5 KB
Loading

‎pics/39.组合总和1.png

183 KB
Loading

‎problems/0039.组合总和.md

Lines changed: 166 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,118 @@
1+
2+
>看懂很容易,彻底掌握需要下功夫
3+
14
#第39题. 组合总和
25

6+
题目链接:https://leetcode-cn.com/problems/combination-sum/
7+
38
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
49

510
candidates 中的数字可以无限制重复被选取。
611

7-
**说明:**
12+
说明:
813

9-
所有数字(包括 target)都是正整数。
10-
解集不能包含重复的组合。 
14+
*所有数字(包括 target)都是正整数。
15+
*解集不能包含重复的组合。 
1116

12-
示例 1:
17+
示例 1:
18+
输入:candidates =[2,3,6,7], target = 7,
19+
所求解集为:
20+
[
21+
[7],
22+
[2,2,3]
23+
]
1324

14-
输入:candidates =[2,3,6,7], target = 7,
15-
所求解集为:
16-
[
17-
[7],
18-
[2,2,3]
19-
]
20-
21-
示例 2:
22-
23-
输入:candidates =[2,3,5], target = 8,
24-
所求解集为:
25-
[
26-
 [2,2,2,2],
27-
 [2,3,3],
28-
 [3,5]
29-
]
25+
示例 2:
26+
输入:candidates =[2,3,5], target = 8,
27+
所求解集为:
28+
[
29+
 [2,2,2,2],
30+
 [2,3,3],
31+
 [3,5]
32+
]
3033

3134
#思路
3235

3336
题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。
3437

38+
本题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)和区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。
3539

36-
这道题上来可以这么想,看看一个数能不能构成target,一个for循环遍历一遍,再看看两个数能不能构成target,两个for循环遍历,在看看三个数能不能构成target,三个for循环遍历,直到candidates.size()个for循环遍历一遍。
40+
本题搜索的过程抽象成树形结构如下:
3741

38-
遇到这种问题,就要想到递归的层级嵌套关系就可以解决这种多层for循环的问题,而回溯则帮我们选择每一个合适的集合!
42+
<imgsrc='../pics/39.组合总和.png'width=600> </img></div>
3943

40-
那么使用回溯的时候,要知道求的是排列,还是组合,排列和组合是不一样的。
44+
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
4145

42-
一些同学可能海分不清,我大概说一下:
46+
而在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) 中都可以知道要递归K层,因为要取k个元素的组合。
4347

44-
**组合是不强调元素顺序的,排列是强调元素顺序的。**
48+
##回溯三部曲
4549

46-
例如 集合 1,2 和 集合 2,1 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,1,2 和 2,1 就是两个集合了。
50+
* 递归函数参数
4751

48-
**求组合,和求排列的回溯写法是不一样的,代码上有小小细节上的改变。**
52+
这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入)
4953

50-
本题选组过程如下:
54+
首先是题目中给出的参数,集合candidates, 和目标值target。
5155

52-
<imgsrc='../pics/39.组合总和.png'width=600> </img></div>
56+
此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。
57+
58+
**本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?**
59+
60+
我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)
61+
62+
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A)
63+
64+
**注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我再讲解排列的时候就重点介绍**
5365

66+
代码如下:
5467

55-
分析完过程,回溯算法的模板框架如下:
5668
```
57-
backtracking() {
58-
if (终止条件) {
59-
存放结果;
60-
}
69+
vector<vector<int>> result;
70+
vector<int> path;
71+
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)
72+
```
6173

62-
for (选择:选择列表(可以想成树中节点孩子的数量)) {
63-
递归,处理节点;
64-
backtracking();
65-
回溯,撤销处理结果
66-
}
74+
* 递归终止条件
75+
76+
在如下树形结构中:
77+
78+
<imgsrc='../pics/39.组合总和.png'width=600> </img></div>
79+
80+
从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target。
81+
82+
sum等于target的时候,需要收集结果,代码如下:
83+
84+
```
85+
if (sum > target) {
86+
return;
87+
}
88+
if (sum == target) {
89+
result.push_back(path);
90+
return;
6791
}
6892
```
6993

70-
按照模板不难写出如下代码,但很一些细节,我在注释中标记出来了。
94+
* 单层搜索的逻辑
95+
96+
单层for循环依然是从startIndex开始,搜索candidates集合。
97+
98+
**注意本题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)的一个区别是:本题元素为可重复选取的**
7199

72-
#C++代码
100+
如何重复选取呢,看代码,注释部分:
73101

74102
```
103+
for (int i = startIndex; i < candidates.size(); i++) {
104+
sum += candidates[i];
105+
path.push_back(candidates[i]);
106+
backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
107+
sum -= candidates[i]; // 回溯
108+
path.pop_back(); // 回溯
109+
}
110+
```
111+
112+
按照[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中给出的模板,不难写出如下C++完整代码:
113+
114+
```
115+
// 版本一
75116
class Solution {
76117
private:
77118
vector<vector<int>> result;
@@ -85,20 +126,102 @@ private:
85126
return;
86127
}
87128
88-
// 这里i 依然从 startIndex开始,因为求的是组合,如果求的是排列,那么i每次都从0开始
89129
for (int i = startIndex; i < candidates.size(); i++) {
90130
sum += candidates[i];
91131
path.push_back(candidates[i]);
92-
backtracking(candidates, target, sum, i); // 关键点在这里,不用i+1了,表示可以重复读取当前的数
132+
backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数
133+
sum -= candidates[i];
134+
path.pop_back();
135+
}
136+
}
137+
public:
138+
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
139+
result.clear();
140+
path.clear();
141+
backtracking(candidates, target, 0, 0);
142+
return result;
143+
}
144+
};
145+
```
146+
147+
##剪枝优化
148+
149+
在这个树形结构中:
150+
151+
<imgsrc='../pics/39.组合总和.png'width=600> </img></div>
152+
153+
以及上面的版本一的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。
154+
155+
其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。
156+
157+
那么可以在for循环的搜索范围上做做文章了。
158+
159+
**对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历**
160+
161+
如图:
162+
163+
<imgsrc='../pics/39.组合总和1.png'width=600> </img></div>
164+
165+
166+
for循环剪枝代码如下:
167+
168+
```
169+
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
170+
```
171+
172+
整体代码如下:(注意注释的部分)
173+
174+
```
175+
class Solution {
176+
private:
177+
vector<vector<int>> result;
178+
vector<int> path;
179+
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
180+
if (sum > target) {
181+
return;
182+
}
183+
if (sum == target) {
184+
result.push_back(path);
185+
return;
186+
}
187+
188+
// 如果 sum + candidates[i] > target 就终止遍历
189+
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
190+
sum += candidates[i];
191+
path.push_back(candidates[i]);
192+
backtracking(candidates, target, sum, i);
93193
sum -= candidates[i];
94194
path.pop_back();
95195
96196
}
97197
}
98198
public:
99199
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
200+
result.clear();
201+
path.clear();
202+
sort(candidates.begin(), candidates.end()); // 需要排序
100203
backtracking(candidates, target, 0, 0);
101204
return result;
102205
}
103206
};
104207
```
208+
209+
#总结
210+
211+
本题和我们之前讲过的[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)有两点不同:
212+
213+
* 组合没有数量要求
214+
* 元素可无限重复选取
215+
216+
针对这两个问题,我都做了详细的分析。
217+
218+
并且给出了对于组合问题,什么时候用startIndex,什么时候不用,并用[回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A)做了对比。
219+
220+
最后还给出了本题的剪枝优化,这个优化如果是初学者的话并不容易想到。
221+
222+
**在求和问题中,排序之后加剪枝是常见的套路!**
223+
224+
可以看出我写的文章都会大量引用之前的文章,就是要不断作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。
225+
226+
**就酱,如果感觉很给力,就帮Carl宣传一波吧,哈哈**
227+

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp