返回 LeetCode 刷题

Markdown File

回溯例题:组合型剪枝

回溯例题--组合型剪枝.md

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 时保存。
Rendered Preview

组合型回溯笔记
康康灵神思路
方法一:枚举下一个数选哪个
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是选定之后还要选的个数

  1. 前提条件

组合型问题问的是:

从 n 个数里面选 k 个
求所有组合

典型题:

  1. 组合
  2. 组合总和 III
  3. 括号生成
  4. 组合总和

最基础例子:

n = 4, k = 2

答案:

[1,2]
[1,3]
[1,4]
[2,3]
[2,4]
[3,4]

  1. 核心概念

组合和子集很像,但区别是:

子集型:
每个长度都要

组合型:
只要长度为 k 的 path

所以组合型保存答案的位置不是一进 dfs 就保存,而是:

if(pathSize == k)
{
保存 path;
return;
}

  1. 固定模板
    void dfs(int start)
    {
    if(pathSize == k)
    {
    保存 path;
    return;
    }

    for(int i = start; i <= n; i++)
    {
    path[pathSize++] = i;

     dfs(i + 1);
    
     pathSize--;
    

    }
    }

核心还是:

选择
递归
撤销

  1. 为什么是 dfs(i + 1)

因为组合不看顺序。

[1,2] 和 [2,1] 是同一个组合

所以一旦选了 i,下一层只能从 i+1 往后选,不能回头。

  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

  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;

}

  1. 和子集型对比
    子集型:

保存答案:
一进入 dfs 就保存

原因:
每个 path 都是答案

循环:
for i = start to numsSize-1


组合型:

保存答案:
pathSize == k 时保存

原因:
只要长度为 k 的 path

循环:
for i = start to n - need + 1
9. 易错点

  1. 组合型不是一进 dfs 就保存。

  2. pathSize == k 才保存。

  3. 递归必须是 dfs(i+1),不能 dfs(start+1)。

  4. 组合不看顺序,所以不能回头选。

  5. 剪枝公式:
    i <= n - (k - pathSize) + 1

  6. 总结
    组合型回溯

问题:
从 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 时保存。