1
- #第40题. 组合总和
1
+ > 这篇可以说是全网把组合问题如何去重,讲的最清晰的了!
2
+
3
+
4
+ #40.组合总和II
5
+
6
+ 题目链接:https://leetcode-cn.com/problems/combination-sum-ii/
7
+
2
8
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
3
9
4
10
candidates 中的每个数字在每个组合中只能使用一次。
5
11
6
- 说明:
12
+ 说明:
13
+ 所有数字(包括目标数)都是正整数。
14
+ 解集不能包含重复的组合。
7
15
8
- 所有数字(包括目标数)都是正整数。
9
- 解集不能包含重复的组合。
16
+ 示例 1:
17
+ 输入: candidates = [ 10,1,2,7,6,1,5] , target = 8,
18
+ 所求解集为:
19
+ [
20
+ [ 1, 7] ,
21
+ [ 1, 2, 5] ,
22
+ [ 2, 6] ,
23
+ [ 1, 1, 6]
24
+ ]
25
+
26
+ 示例 2:
27
+ 输入: candidates = [ 2,5,2,1,2] , target = 5,
28
+ 所求解集为:
29
+ [
30
+ [ 1,2,2] ,
31
+ [ 5]
32
+ ]
10
33
11
- 示例 1:
34
+ # 思路
12
35
13
- 输入: candidates = [ 10,1,2,7,6,1,5] , target = 8,
14
- 所求解集为:
15
- [
16
- [ 1, 7] ,
17
- [ 1, 2, 5] ,
18
- [ 2, 6] ,
19
- [ 1, 1, 6]
20
- ]
36
+ 这道题目和[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 如下区别:
21
37
22
- 示例 2:
38
+ 1 . 本题candidates 中的每个数字在每个组合中只能使用一次。
39
+ 2 . 本题数组candidates的元素是有重复的,而[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 是无重复元素的数组candidates
23
40
24
- 输入: candidates = [ 2,5,2,1,2] , target = 5,
25
- 所求解集为:
26
- [
27
- [ 1,2,2] ,
28
- [ 5]
29
- ]
41
+ 最后本题和[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 要求一样,解集不能包含重复的组合。
30
42
31
- # 思想
43
+ ** 本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合 ** 。
32
44
33
- 这道题目和 [ 0039.组合总和 ] ( https://github.com/youngyangyang04/leetcode/blob/master/problems/0039.组合总和.md ) 区别就是要去重。
45
+ 一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时!
34
46
35
- 很多同学在去重上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
47
+ 所以要在搜索的过程中就去掉重复组合。
36
48
37
- 这个去重为什么很难理解呢, ** 所谓去重,其实就是使用过的元素不能重复选取。 ** 这么一说好像很简单!
49
+ 很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
38
50
51
+ 这个去重为什么很难理解呢,** 所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单!
39
52
40
53
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。** 没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。**
41
54
42
- 所以要明确我们要去重的是同一树层上的“使用过”。
55
+ 那么问题来了,我们是要同一树层上使用过,还是统一树枝上使用过呢?
56
+
57
+ 回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
58
+
59
+ ** 所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重** 。
43
60
44
61
为了理解去重我们来举一个例子,candidates =[ 1, 1, 2] , target = 3,(方便起见candidates已经排序了)
45
62
46
- 选择过程如图所示 :
63
+ 选择过程树形结构如图所示 :
47
64
48
65
<img src =' ../pics/40.组合总和II.png ' width =600 > </img ></div >
49
66
67
+ 可以看到图中,每个节点相对于[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 我多加了used数组,这个used数组下面会重点介绍。
68
+
69
+ ##回溯三部曲
70
+
71
+ * ** 递归函数参数**
72
+
73
+ 与[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。
74
+
75
+ 这个集合去重的重任就是used来完成的。
76
+
77
+ 代码如下:
50
78
51
- 理解了“同一树枝使用过”和“同一树层使用过” 之后,我们在拉看如下代码实现,关键地方已经注释,大家应该就理解了
79
+ ```
80
+ vector<vector<int>> result; // 存放组合集合
81
+ vector<int> path; // 符合条件的组合
82
+ void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
83
+ ```
84
+
85
+ * ** 递归终止条件**
86
+
87
+ 与[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 相同,终止条件为` sum > target ` 和` sum == target ` 。
52
88
53
- #C++代码
89
+ 代码如下:
90
+
91
+ ```
92
+ if (sum > target) { // 这个条件其实可以省略
93
+ return;
94
+ }
95
+ if (sum == target) {
96
+ result.push_back(path);
97
+ return;
98
+ }
99
+ ```
100
+
101
+ ` sum > target ` 这个条件其实可以省略,因为和在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。
102
+
103
+ * ** 单层搜索的逻辑**
104
+
105
+ 这里与[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 最大的不同就是要去重了。
106
+
107
+ 前面我们提到:要去重的是“同一树层上的使用过”,如果判断同一树层上元素(相同的元素)是否使用过了呢。
108
+
109
+ ** 如果` candidates[i] == candidates[i - 1] ` 并且` used[i - 1] == false ` ,就说明:前一个树枝,使用了candidates[ i - 1] ,也就是说同一树层使用过candidates[ i - 1] ** 。
110
+
111
+ 此时for循环里就应该做continue的操作。
112
+
113
+ 这块比较抽象,如图:
114
+
115
+ <img src =' ../pics/40.组合总和II1.png ' width =600 > </img ></div >
116
+
117
+ 我在图中将used的变化用橘黄色标注上,可以看出在candidates[ i] == candidates[ i - 1] 相同的情况下:
118
+
119
+ * used[ i - 1] == true,说明同一树支candidates[ i - 1] 使用过
120
+ * used[ i - 1] == false,说明同一树层candidates[ i - 1] 使用过
121
+
122
+ ** 这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!**
123
+
124
+ 那么单层搜索的逻辑代码如下:
125
+
126
+ ```
127
+ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
128
+ // used[i - 1] == true,说明同一树支candidates[i - 1]使用过
129
+ // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
130
+ // 要对同一树层使用过的元素进行跳过
131
+ if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
132
+ continue;
133
+ }
134
+ sum += candidates[i];
135
+ path.push_back(candidates[i]);
136
+ used[i] = true;
137
+ backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
138
+ used[i] = false;
139
+ sum -= candidates[i];
140
+ path.pop_back();
141
+ }
142
+ ```
143
+
144
+ ** 注意sum + candidates[ i] <= target为剪枝操作,在[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 有讲解过!**
145
+
146
+ ##C++代码
147
+
148
+ 回溯三部曲分析完了,整体C++代码如下:
54
149
55
150
```
56
151
class Solution {
57
152
private:
58
153
vector<vector<int>> result;
59
154
vector<int> path;
60
155
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
61
- if (sum > target) {
62
- return;
63
- }
64
156
if (sum == target) {
65
157
result.push_back(path);
66
158
return;
67
159
}
68
-
69
- // 每个组合中只能使用一次 所以用 startindex
70
- // 给定一个数组 candidates 默认有重复项,解集不能包含重复的组合。 所以使用if这一套
71
- for (int i = startIndex; i < candidates.size(); i++) {
72
- // 这里理解used[i - 1]非常重要
73
- // used[i - 1] == true,说明同一树支candidates[i - 1]使用过
160
+ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
161
+ // used[i - 1] == true,说明同一树支candidates[i - 1]使用过
74
162
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
75
- //而我们要对同一树层使用过的元素进行跳过
76
- if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
163
+ //要对同一树层使用过的元素进行跳过
164
+ if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
77
165
continue;
78
166
}
79
167
sum += candidates[i];
80
168
path.push_back(candidates[i]);
81
169
used[i] = true;
82
- backtracking(candidates, target, sum, i + 1, used); //关键点在这里,不用i+1了
170
+ backtracking(candidates, target, sum, i + 1, used); //和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
83
171
used[i] = false;
84
172
sum -= candidates[i];
85
173
path.pop_back();
@@ -89,11 +177,25 @@ private:
89
177
public:
90
178
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
91
179
vector<bool> used(candidates.size(), false);
92
- // 首先把给candidates排序,让其相同的元素都挨在一起。
180
+ path.clear();
181
+ result.clear();
182
+ // 首先把给candidates排序,让其相同的元素都挨在一起。
93
183
sort(candidates.begin(), candidates.end());
94
184
backtracking(candidates, target, 0, 0, used);
95
185
return result;
96
-
97
186
}
98
187
};
188
+
99
189
```
190
+
191
+ #总结
192
+
193
+ 本题同样是求组合总和,但就是因为其数组candidates有重复元素,而要求不能有重复的组合,所以相对于[ 39.组合总和] ( https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw ) 难度提升了不少。
194
+
195
+ ** 关键是去重的逻辑,代码很简单,网上一搜一大把,但几乎没有能把这块代码含义讲明白的,基本都是给出代码,然后说这就是去重了,究竟怎么个去重法也是模棱两可** 。
196
+
197
+ 所以Carl有必要把去重的这块彻彻底底的给大家讲清楚,** 就连“树层去重”和“树枝去重”都是我自创的词汇,希望对大家理解有帮助!**
198
+
199
+ ** 就酱,如果感觉「代码随想录」诚意满满,就帮Carl宣传一波吧,感谢啦!**
200
+
201
+