1
+
1
2
# 第77题. 组合
3
+
4
+ 题目链接:https://leetcode-cn.com/problems/combinations/
5
+
2
6
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
3
7
示例:
4
8
13
17
[ 1,4] ,
14
18
]
15
19
20
+
16
21
# 思路
17
22
18
23
这是回溯法的经典题目。
33
38
那么就三层for循环,代码如下:
34
39
35
40
```
36
- for (int i = 1; i <= n; i++) {
37
- for (int j = i + 1; j <= n; j++) {
38
- for (int u = j + 1; u <=n; n++) {
41
+ for (int i = 1; i <= n; i++) {
42
+ for (int j = i + 1; j <= n; j++) {
43
+ for (int u = j + 1; u <=n; n++) {
39
44
40
- }
41
45
}
42
46
}
47
+ }
43
48
```
44
- ** 如果n为 100,k为50呢,那就50层for循环,是不是开始窒息。**
49
+
50
+ ** 如果n为100,k为50呢,那就50层for循环,是不是开始窒息** 。
45
51
46
52
那么回溯法就能解决这个问题了。
47
53
48
- 回溯是用来做选择的,递归用来节点层叠嵌套 ,** 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 **
54
+ 回溯是用来做选择,递归用来做节点层叠嵌套(可以理解是随便开K的for循环) ,** 每一次的递归相当于嵌套一个for循环,可以用于解决多层嵌套循环的问题了 ** 。
49
55
50
- 其实子集和组合问题都可以抽象为一个树形结构,如下:
56
+ ** 回溯问题都可以抽象为一棵树形结构!用树形结构来理解回溯就容易多了 ** 。
51
57
58
+ 那么我们把组合问题抽象为如下树形结构:
52
59
53
60
<img src =' ../pics/77.组合.png ' width =600 > </img ></div >
54
61
55
- 可以看一下这个棵树,一开始集合是 1,2,3,4, 从左向右去数,取过的数,不在重复取。
62
+ 可以看出这个棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不在重复取。
63
+
64
+ 第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取,2,3,4, 得到集合[ 1,2] [ 1,3] [ 1,4] ,以此类推。
65
+
66
+ ** 回溯的问题都可以抽象为一个树形结构,在求解组合问题的过程中,n相当于树的宽度,k相当于树的深度** 。
67
+
68
+ ** 每次从集合中选组元素,可选择的范围随着选择的进行而限缩,调整可选择的范围**
69
+
70
+ 如何在这个树上遍历,然后收集到我们要的结果集呢?
56
71
57
- 第一取1,集合变为2,3,4 ,因为k为2,我们只需要去一个数就可以了,分别取,2,3,4, 得到集合 [ 1,2 ] [ 1,3 ] [ 1,4 ] ,以此类推 。
72
+ 用的就是回溯搜索法, ** 可以发现,每次搜索到了叶子节点,我们就找到了一个结果 ** 。
58
73
59
- ** 其实这就转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围**
60
74
61
- 如何在这个树上遍历,然后收集到我们要的结果集呢,用的就是回溯搜索法,** 可以发现,每次搜索到了叶子节点,我们就找到了一个结果。**
75
+ ** 这份模板,大家可以要记住了,后面做回溯搜索的题目,都离不开这个模板** 。
76
+
77
+ ## 求组合
78
+
79
+ 掌握了模板之后,我们再来看一下这道求组合的题目。
80
+
81
+ * 回溯函数返回值以及参数
82
+
83
+ 在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
84
+
85
+ 代码如下:
62
86
63
- 分析完过程,我们来看一下 回溯算法的模板框架如下:
64
87
```
65
- backtracking() {
66
- if (终止条件) {
67
- 存放结果;
68
- }
88
+ vector<vector<int>> result; // 存放符合条件结果的集合
89
+ vector<int> path; // 用来存放符合条件结果
90
+ ```
69
91
70
- for (选择:选择列表(可以想成树中节点孩子的数量)) {
71
- 递归,处理节点;
72
- backtracking();
73
- 回溯,撤销处理结果
74
- }
92
+ 其实不定义这两个全局遍历也是可以的,把这两个变量放进回溯函数的参数里,但为了函数里参数太多影响可读性,所以我定义全局变量。
93
+
94
+ 首先两个参数,集合n里面取k的数,是两个int型的变量。
95
+
96
+ 然后还需要一个参数,也为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[ 1,...,n] )。
97
+
98
+ 为什么要有这个startIndex呢?
99
+
100
+ 从下图中红线部分可以看出,在集合[ 1,2,3,4] 取1之后,下一层递归,就要在[ 2,3,4] 中取数了,那么下一层递归如何知道从[ 2,3,4] 中取数呢,靠的就是startIndex。
101
+
102
+ <img src =' ../pics/77.组合2.png ' width =600 > </img ></div >
103
+
104
+ 所以需要startIndex来记录下一层递归,搜索的起始位置。
105
+
106
+ 那么整体代码如下:
107
+
108
+ ```
109
+ vector<vector<int>> result; // 存放符合条件结果的集合
110
+ vector<int> path; // 用来存放符合条件单一结果
111
+ void backtracking(int n, int k, int startIndex)
112
+ ```
113
+
114
+ * 回溯函数终止条件
115
+
116
+ 什么时候到达所谓的叶子节点了呢?
117
+
118
+ 就是path这个数组的大小如果达到k,说明我们找到了一个集合大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
119
+
120
+ 如图红色部分:
121
+
122
+ <img src =' ../pics/77.组合3.png ' width =600 > </img ></div >
123
+
124
+ 此时用result二维数组,把path保存起来,并终止本层递归。
125
+
126
+ 所以终止条件代码如下:
127
+
128
+ ```
129
+ if (path.size() == k) {
130
+ result.push_back(path);
131
+ return;
75
132
}
76
133
```
77
134
78
- 分析模板:
79
135
80
- 什么是达到了终止条件,树中就可以看出,搜到了叶子节点了,就找到了一个符合题目要求的答案,就把这个答案存放起来。
136
+ * 回溯搜索的遍历过程
81
137
82
- 看一下这个for循环,这个for循环是做什么的,for 就是处理树中节点各个孩子的情况, 一个节点有多少个孩子,这个for循环就执行多少次。
138
+ 在如下如中,我们知道for循环用来横向遍历,递归的过程是纵向遍历。
139
+ <img src =' ../pics/77.组合1.png ' width =600 > </img ></div >
83
140
84
- 最后就要看这个递归的过程了,注意这个backtracking就是自己调用自己,实现递归 。
141
+ 如此我们才遍历完图中的这棵树 。
85
142
86
- 一些同学对递归操作本来就不熟练,递归上面又加上一个for循环,可能就更迷糊了, 我来给大家捋顺一下。
143
+ 那么for循环每次就是从startIndex开始遍历,然后用path保存每次遍历到的节点。
87
144
88
- 这个backtracking 其实就是向树的叶子节点方向遍历, for循环可以理解是横向遍历,backtracking 就是纵向遍历,这样就把这棵树全遍历完了。
145
+ 代码如下:
89
146
90
- 那么backtracking就是一直往深处遍历,总会遇到叶子节点,遇到了叶子节点,就要返回,那么backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。
147
+ ```
148
+ for (int i = startIndex; i <= n; i++) {
149
+ path.push_back(i); // 处理节点
150
+ backtracking(n, k, i + 1); // 注意下一层搜索要从i+1开始
151
+ path.pop_back(); // 回溯,撤销处理的节点
152
+ }
153
+ ```
91
154
92
- 分析完模板,本题代码如下 :
155
+ 关键地方都讲完了,组合问题C++完整代码如下 :
93
156
94
- # C++ 代码
95
157
96
158
```
97
159
class Solution {
@@ -103,16 +165,16 @@ private:
103
165
result.push_back(path);
104
166
return;
105
167
}
106
- // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
107
168
for (int i = startIndex; i <= n; i++) {
108
169
path.push_back(i); // 处理节点
109
170
backtracking(n, k, i + 1);
110
171
path.pop_back(); // 回溯,撤销处理的节点
111
172
}
112
173
}
113
174
public:
114
-
115
175
vector<vector<int>> combine(int n, int k) {
176
+ result.clear(); // 可以不写
177
+ path.clear(); // 可以不写
116
178
backtracking(n, k, 1);
117
179
return result;
118
180
}
@@ -145,7 +207,7 @@ for (int i = startIndex; i <= n; i++)
145
207
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
146
208
```
147
209
148
- 整体代码如下 :
210
+ 优化后整体代码如下 :
149
211
150
212
```
151
213
class Solution {
@@ -157,7 +219,6 @@ private:
157
219
result.push_back(path);
158
220
return;
159
221
}
160
- // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
161
222
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
162
223
path.push_back(i); // 处理节点
163
224
backtracking(n, k, i + 1);
@@ -174,6 +235,3 @@ public:
174
235
```
175
236
176
237
177
-
178
- # 观后感
179
- 我来写一下观后感: 很厉害,转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围。 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 每一层递归中,尽量节省循环次数,这样在后续的递归调用中,节省下来的循环会被以至少指数等级放大。
0 commit comments