1+
12#第77题. 组合
3+
4+ 题目链接:https://leetcode-cn.com/problems/combinations/
5+
26给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
37示例:
48
1317[ 1,4] ,
1418]
1519
20+
1621#思路
1722
1823这是回溯法的经典题目。
3338那么就三层for循环,代码如下:
3439
3540```
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++) {
3944
40- }
4145 }
4246 }
47+ }
4348```
44- ** 如果n为 100,k为50呢,那就50层for循环,是不是开始窒息。**
49+
50+ ** 如果n为100,k为50呢,那就50层for循环,是不是开始窒息** 。
4551
4652那么回溯法就能解决这个问题了。
4753
48- 回溯是用来做选择的,递归用来节点层叠嵌套 ,** 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 **
54+ 回溯是用来做选择,递归用来做节点层叠嵌套(可以理解是随便开K的for循环) ,** 每一次的递归相当于嵌套一个for循环,可以用于解决多层嵌套循环的问题了 ** 。
4955
50- 其实子集和组合问题都可以抽象为一个树形结构,如下:
56+ ** 回溯问题都可以抽象为一棵树形结构!用树形结构来理解回溯就容易多了 ** 。
5157
58+ 那么我们把组合问题抽象为如下树形结构:
5259
5360<img src =' ../pics/77.组合.png ' width =600 > </img ></div >
5461
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+ 如何在这个树上遍历,然后收集到我们要的结果集呢?
5671
57- 第一取1,集合变为2,3,4 ,因为k为2,我们只需要去一个数就可以了,分别取,2,3,4, 得到集合 [ 1,2 ] [ 1,3 ] [ 1,4 ] ,以此类推 。
72+ 用的就是回溯搜索法, ** 可以发现,每次搜索到了叶子节点,我们就找到了一个结果 ** 。
5873
59- ** 其实这就转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围**
6074
61- 如何在这个树上遍历,然后收集到我们要的结果集呢,用的就是回溯搜索法,** 可以发现,每次搜索到了叶子节点,我们就找到了一个结果。**
75+ ** 这份模板,大家可以要记住了,后面做回溯搜索的题目,都离不开这个模板** 。
76+
77+ ##求组合
78+
79+ 掌握了模板之后,我们再来看一下这道求组合的题目。
80+
81+ * 回溯函数返回值以及参数
82+
83+ 在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
84+
85+ 代码如下:
6286
63- 分析完过程,我们来看一下 回溯算法的模板框架如下:
6487```
65- backtracking() {
66- if (终止条件) {
67- 存放结果;
68- }
88+ vector<vector<int>> result; // 存放符合条件结果的集合
89+ vector<int> path; // 用来存放符合条件结果
90+ ```
6991
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;
75132}
76133```
77134
78- 分析模板:
79135
80- 什么是达到了终止条件,树中就可以看出,搜到了叶子节点了,就找到了一个符合题目要求的答案,就把这个答案存放起来。
136+ * 回溯搜索的遍历过程
81137
82- 看一下这个for循环,这个for循环是做什么的,for 就是处理树中节点各个孩子的情况, 一个节点有多少个孩子,这个for循环就执行多少次。
138+ 在如下如中,我们知道for循环用来横向遍历,递归的过程是纵向遍历。
139+ <img src =' ../pics/77.组合1.png ' width =600 > </img ></div >
83140
84- 最后就要看这个递归的过程了,注意这个backtracking就是自己调用自己,实现递归 。
141+ 如此我们才遍历完图中的这棵树 。
85142
86- 一些同学对递归操作本来就不熟练,递归上面又加上一个for循环,可能就更迷糊了, 我来给大家捋顺一下。
143+ 那么for循环每次就是从startIndex开始遍历,然后用path保存每次遍历到的节点。
87144
88- 这个backtracking 其实就是向树的叶子节点方向遍历, for循环可以理解是横向遍历,backtracking 就是纵向遍历,这样就把这棵树全遍历完了。
145+ 代码如下:
89146
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+ ```
91154
92- 分析完模板,本题代码如下 :
155+ 关键地方都讲完了,组合问题C++完整代码如下 :
93156
94- #C++ 代码
95157
96158```
97159class Solution {
@@ -103,16 +165,16 @@ private:
103165 result.push_back(path);
104166 return;
105167 }
106- // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
107168 for (int i = startIndex; i <= n; i++) {
108169 path.push_back(i); // 处理节点
109170 backtracking(n, k, i + 1);
110171 path.pop_back(); // 回溯,撤销处理的节点
111172 }
112173 }
113174public:
114-
115175 vector<vector<int>> combine(int n, int k) {
176+ result.clear(); // 可以不写
177+ path.clear(); // 可以不写
116178 backtracking(n, k, 1);
117179 return result;
118180 }
@@ -145,7 +207,7 @@ for (int i = startIndex; i <= n; i++)
145207for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
146208```
147209
148- 整体代码如下 :
210+ 优化后整体代码如下 :
149211
150212```
151213class Solution {
@@ -157,7 +219,6 @@ private:
157219 result.push_back(path);
158220 return;
159221 }
160- // 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
161222 for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
162223 path.push_back(i); // 处理节点
163224 backtracking(n, k, i + 1);
@@ -174,6 +235,3 @@ public:
174235```
175236
176237
177-
178- #观后感
179- 我来写一下观后感: 很厉害,转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围。 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 每一层递归中,尽量节省循环次数,这样在后续的递归调用中,节省下来的循环会被以至少指数等级放大。