1组合型回溯笔记
2康康灵神思路
3方法一:枚举下一个数选哪个
4class Solution:
5 def combine(self, n: int, k: int) -> List[List[int]]:
6 ans = []
7 path = []
8
9 # 枚举选哪个:在 1 到 i 中选一个数,加到 path 末尾
10 def dfs(i: int) -> None:
11 d = k - len(path) # 还要选 d 个数
12 if d == 0: # 选好了
13 ans.append(path.copy())
14 return
15
16 # 枚举的数不能太小,否则后面没有数可以选
17 for j in range(i, d - 1, -1):
18 path.append(j)
19 dfs(j - 1)
20 path.pop() # 恢复现场
21 dfs(n) # 从 n 开始倒着枚举
22 return ans
23
24复杂度分析
25时间复杂度:分析回溯问题的时间复杂度,有一个简易公式:路径长度×搜索树的叶子数。对于本题,路径长度始终为 k,叶子个数为 C(n,k),所以时间复杂度为 O(k⋅C(n,k))。
26空间复杂度:O(k)。返回值不计入。
27
28方法二:选或不选
29class Solution:
30 def combine(self, n: int, k: int) -> List[List[int]]:
31 ans = []
32 path = []
33
34 # 选或不选:讨论 i 是否加入 path
35 def dfs(i: int) -> None:
36 d = k - len(path) # 还要选 d 个数
37 if d == 0: # 选好了
38 ans.append(path.copy())
39 return
40
41 # 不选 i
42 if i > d:
43 dfs(i - 1)
44
45 # 选 i
46 path.append(i)
47 dfs(i - 1)
48 path.pop() # 恢复现场
49
50 dfs(n) # 从 i=n 开始倒着枚举
51 return ans
52灵神这里是倒着回溯,仅提供从n个里面选k个,d是选定之后还要选的个数
531. 前提条件
54
55组合型问题问的是:
56
57从 n 个数里面选 k 个
58求所有组合
59
60典型题:
61
6277. 组合
63216. 组合总和 III
6422. 括号生成
6539. 组合总和
66
67最基础例子:
68
69n = 4, k = 2
70
71答案:
72
73[1,2]
74[1,3]
75[1,4]
76[2,3]
77[2,4]
78[3,4]
79
802. 核心概念
81
82组合和子集很像,但区别是:
83
84子集型:
85 每个长度都要
86
87组合型:
88 只要长度为 k 的 path
89
90所以组合型保存答案的位置不是一进 dfs 就保存,而是:
91
92if(pathSize == k)
93{
94 保存 path;
95 return;
96}
97
983. 固定模板
99void dfs(int start)
100{
101 if(pathSize == k)
102 {
103 保存 path;
104 return;
105 }
106
107 for(int i = start; i <= n; i++)
108 {
109 path[pathSize++] = i;
110
111 dfs(i + 1);
112
113 pathSize--;
114 }
115}
116
117核心还是:
118
119选择
120递归
121撤销
122
1234. 为什么是 dfs(i + 1)
124
125因为组合不看顺序。
126
127[1,2] 和 [2,1] 是同一个组合
128
129所以一旦选了 i,下一层只能从 i+1 往后选,不能回头。
130
1315. 用 n=4,k=2 推一遍
132
133开始:
134
135path=[]
136start=1
137
138选 1:
139
140path=[1]
141
142下一层从 2 开始:
143
144选2 -> [1,2] 保存
145选3 -> [1,3] 保存
146选4 -> [1,4] 保存
147
148回到第一层,选 2:
149
150path=[2]
151
152下一层从 3 开始:
153
154选3 -> [2,3] 保存
155选4 -> [2,4] 保存
156
157选 3:
158
159path=[3]
160
161下一层从 4 开始:
162
163选4 -> [3,4] 保存
164
165最终:
166
167[1,2]
168[1,3]
169[1,4]
170[2,3]
171[2,4]
172[3,4]
1736. 剪枝优化
174
175原始循环:
176
177for(int i = start; i <= n; i++)
178
179有时候后面数字不够选了,还会继续递归,浪费。
180
181比如:
182
183n=5, k=3
184pathSize=1
185
186还需要:
187
188need = k - pathSize = 2
189
190如果从 i=5 开始,后面只剩一个数 5,不够选 2 个。
191
192这里是正着回溯,也就是说need是还需要选的数,为了保证后面i+1的回溯情况,所以要保持第i个,所以是n-need+1
193
194所以循环可以写成:
195
196int need = k - pathSize;
197
198for(int i = start; i <= n - need + 1; i++)
199
200这个公式很重要:
201
202i 最晚只能到 n - need + 1
203
2047. C 代码打印版
205#include <stdio.h>
206
207#define MAXN 30
208
209int path[MAXN];
210int pathSize = 0;
211int n;
212int k;
213
214void printPath()
215{
216 printf("[");
217 for(int i = 0; i < pathSize; i++)
218 {
219 if(i > 0)
220 {
221 printf(",");
222 }
223 printf("%d", path[i]);
224 }
225 printf("]\n");
226}
227
228void dfs(int start)
229{
230 if(pathSize == k)
231 {
232 printPath();
233 return;
234 }
235
236 int need = k - pathSize;
237
238 for(int i = start; i <= n - need + 1; i++)
239 {
240 path[pathSize++] = i;
241
242 dfs(i + 1);
243
244 pathSize--;
245 }
246}
247int main()
248{
249 n = 4;
250 k = 2;
251
252 dfs(1);
253
254 return 0;
255}
256
2578. 和子集型对比
258子集型:
259
260保存答案:
261 一进入 dfs 就保存
262
263原因:
264 每个 path 都是答案
265
266循环:
267 for i = start to numsSize-1
268
269--------------------------------
270
271组合型:
272
273保存答案:
274 pathSize == k 时保存
275
276原因:
277 只要长度为 k 的 path
278
279循环:
280 for i = start to n - need + 1
2819. 易错点
2821. 组合型不是一进 dfs 就保存。
283
2842. pathSize == k 才保存。
285
2863. 递归必须是 dfs(i+1),不能 dfs(start+1)。
287
2884. 组合不看顺序,所以不能回头选。
289
2905. 剪枝公式:
291 i <= n - (k - pathSize) + 1
292
29310. 总结
294组合型回溯
295
296问题:
297 从 n 个数里选 k 个
298
299核心:
300 pathSize == k 时保存答案
301
302变量:
303 start 表示下一层从哪里开始选
304 pathSize 表示当前选了几个
305 k 表示目标数量
306
307模板:
308 if(pathSize == k)
309 保存答案
310
311 for i from start to n - need + 1
312 选择 i
313 dfs(i+1)
314 撤销 i
315
316主要还是考虑need和k和pathSize的关系从而实现减小范围剪枝
317 start
318 k
319 pathSize
320 need
321 剪枝
322
323组合型回溯 = 固定长度的子集,只在 pathSize == k 时保存。
组合型回溯笔记
康康灵神思路
方法一:枚举下一个数选哪个
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
ans = []
path = []
# 枚举选哪个:在 1 到 i 中选一个数,加到 path 末尾
def dfs(i: int) -> None:
d = k - len(path) # 还要选 d 个数
if d == 0: # 选好了
ans.append(path.copy())
return
# 枚举的数不能太小,否则后面没有数可以选
for j in range(i, d - 1, -1):
path.append(j)
dfs(j - 1)
path.pop() # 恢复现场
dfs(n) # 从 n 开始倒着枚举
return ans
复杂度分析
时间复杂度:分析回溯问题的时间复杂度,有一个简易公式:路径长度×搜索树的叶子数。对于本题,路径长度始终为 k,叶子个数为 C(n,k),所以时间复杂度为 O(k⋅C(n,k))。
空间复杂度:O(k)。返回值不计入。
方法二:选或不选
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
ans = []
path = []
# 选或不选:讨论 i 是否加入 path
def dfs(i: int) -> None:
d = k - len(path) # 还要选 d 个数
if d == 0: # 选好了
ans.append(path.copy())
return
# 不选 i
if i > d:
dfs(i - 1)
# 选 i
path.append(i)
dfs(i - 1)
path.pop() # 恢复现场
dfs(n) # 从 i=n 开始倒着枚举
return ans
灵神这里是倒着回溯,仅提供从n个里面选k个,d是选定之后还要选的个数
- 前提条件
组合型问题问的是:
从 n 个数里面选 k 个
求所有组合
典型题:
- 组合
- 组合总和 III
- 括号生成
- 组合总和
最基础例子:
n = 4, k = 2
答案:
[1,2]
[1,3]
[1,4]
[2,3]
[2,4]
[3,4]
- 核心概念
组合和子集很像,但区别是:
子集型:
每个长度都要
组合型:
只要长度为 k 的 path
所以组合型保存答案的位置不是一进 dfs 就保存,而是:
if(pathSize == k)
{
保存 path;
return;
}
-
固定模板
void dfs(int start)
{
if(pathSize == k)
{
保存 path;
return;
}
for(int i = start; i <= n; i++)
{
path[pathSize++] = i;
dfs(i + 1);
pathSize--;
}
}
核心还是:
选择
递归
撤销
- 为什么是 dfs(i + 1)
因为组合不看顺序。
[1,2] 和 [2,1] 是同一个组合
所以一旦选了 i,下一层只能从 i+1 往后选,不能回头。
- 用 n=4,k=2 推一遍
开始:
path=[]
start=1
选 1:
path=[1]
下一层从 2 开始:
选2 -> [1,2] 保存
选3 -> [1,3] 保存
选4 -> [1,4] 保存
回到第一层,选 2:
path=[2]
下一层从 3 开始:
选3 -> [2,3] 保存
选4 -> [2,4] 保存
选 3:
path=[3]
下一层从 4 开始:
选4 -> [3,4] 保存
最终:
[1,2]
[1,3]
[1,4]
[2,3]
[2,4]
[3,4]
6. 剪枝优化
原始循环:
for(int i = start; i <= n; i++)
有时候后面数字不够选了,还会继续递归,浪费。
比如:
n=5, k=3
pathSize=1
还需要:
need = k - pathSize = 2
如果从 i=5 开始,后面只剩一个数 5,不够选 2 个。
这里是正着回溯,也就是说need是还需要选的数,为了保证后面i+1的回溯情况,所以要保持第i个,所以是n-need+1
所以循环可以写成:
int need = k - pathSize;
for(int i = start; i <= n - need + 1; i++)
这个公式很重要:
i 最晚只能到 n - need + 1
- C 代码打印版
#include <stdio.h>
#define MAXN 30
int path[MAXN];
int pathSize = 0;
int n;
int k;
void printPath()
{
printf("[");
for(int i = 0; i < pathSize; i++)
{
if(i > 0)
{
printf(",");
}
printf("%d", path[i]);
}
printf("]\n");
}
void dfs(int start)
{
if(pathSize == k)
{
printPath();
return;
}
int need = k - pathSize;
for(int i = start; i <= n - need + 1; i++)
{
path[pathSize++] = i;
dfs(i + 1);
pathSize--;
}
}
int main()
{
n = 4;
k = 2;
dfs(1);
return 0;
}
- 和子集型对比
子集型:
保存答案:
一进入 dfs 就保存
原因:
每个 path 都是答案
循环:
for i = start to numsSize-1
组合型:
保存答案:
pathSize == k 时保存
原因:
只要长度为 k 的 path
循环:
for i = start to n - need + 1
9. 易错点
-
组合型不是一进 dfs 就保存。
-
pathSize == k 才保存。
-
递归必须是 dfs(i+1),不能 dfs(start+1)。
-
组合不看顺序,所以不能回头选。
-
剪枝公式:
i <= n - (k - pathSize) + 1
-
总结
组合型回溯
问题:
从 n 个数里选 k 个
核心:
pathSize == k 时保存答案
变量:
start 表示下一层从哪里开始选
pathSize 表示当前选了几个
k 表示目标数量
模板:
if(pathSize == k)
保存答案
for i from start to n - need + 1
选择 i
dfs(i+1)
撤销 i
主要还是考虑need和k和pathSize的关系从而实现减小范围剪枝
start
k
pathSize
need
剪枝
组合型回溯 = 固定长度的子集,只在 pathSize == k 时保存。