返回 LeetCode 刷题

Markdown File

回溯例题:全排列

回溯例题--全排列.md

1看一下灵神的代码
2class Solution:
3 def permute(self, nums: List[int]) -> List[List[int]]:
4 n = len(nums)
5 path = [0] * n
6 ans = []
7
8 # 枚举 path[i] 填 remain(剩余数字)中的哪个数
9 def dfs(i: int, remain: Set[int]) -> None:
10 if i == n:
11 ans.append(path.copy())
12 return
13
14 for x in remain:
15 path[i] = x
16 dfs(i + 1, remain - {x})
17
18 dfs(0, set(nums))
19 return ans
20
21用bool判断用过没用过就是下面这种
22class Solution:
23 def permute(self, nums: List[int]) -> List[List[int]]:
24 n = len(nums)
25 path = [0] * n # 所有排列的长度都是一样的 n
26 on_path = [False] * n
27 ans = []
28
29 # 枚举 path[i] 填 nums 的哪个数
30 def dfs(i: int) -> None:
31 if i == n:
32 ans.append(path.copy()) # 也可以写 path[:]
33 return
34 for j, on in enumerate(on_path):
35 if not on:
36 path[i] = nums[j] # 从没有选的数字中选一个
37 on_path[j] = True # 已选上
38 dfs(i + 1)
39 on_path[j] = False # 恢复现场
40 # 注意 path 无需恢复现场,因为排列长度固定,直接覆盖就行
41
42 dfs(0)
43 return ans
44
45复杂度分析
46时间复杂度:O(n⋅n!),其中 n 为 nums 的长度。视频中提到,搜索树中的节点个数低于 3⋅n!。实际上,精确值为 ⌊e⋅n!⌋,其中 e=2.718⋯ 为自然常数。有 O(n!) 个叶节点,每个叶节点花费 O(n) 的时间复制 path 数组,因此时间复杂度为 O(n⋅n!)。
47空间复杂度:O(n)。返回值的空间不计入。
48
49排列型回溯:46 全排列
501. 前提条件
51
52全排列问的是:
53
54给 nums 里的所有数,重新排列出所有顺序。
55
56例如:
57
58nums = [1,2,3]
59
60答案:
61
62[1,2,3]
63[1,3,2]
64[2,1,3]
65[2,3,1]
66[3,1,2]
67[3,2,1]
68
69核心和组合不同:
70
71组合:不能回头选,靠 start
72排列:每一层都可以从头选,靠 used[]
73
742. 核心概念
75
76排列型每一层是在问:
77
78当前位置放谁?
79
80比如 [1,2,3]:
81
82第 0 个位置:可以放 1/2/3
83第 1 个位置:放剩下没用过的
84第 2 个位置:放最后一个
85
86所以需要:int used[MAXN];
87
88表示:
89
90这个数有没有被当前 path 用过
91
923. 固定模板
93void dfs()
94{
95 if(pathSize == numsSize)
96 {
97 保存 path;
98 return;
99 }
100
101 for(int i = 0; i < numsSize; i++)
102 {
103 if(used[i])
104 {
105 continue;
106 }
107
108 used[i] = 1;
109 path[pathSize++] = nums[i];
110
111 dfs();
112
113 pathSize--;
114 used[i] = 0;
115 }
116}
117
118核心还是:
119
120选择
121递归
122撤销
123
124只不过排列型撤销两个东西:
125
126pathSize--
127used[i] = 0
128
1294. 为什么不用 start?
130
131组合型:
132
133[1,2] 和 [2,1] 算同一个
134
135所以用:
136
137dfs(i + 1);
138
139防止回头。
140
141排列型:
142
143[1,2] 和 [2,1] 是两个不同答案
144
145所以每一层都要:
146
147for(int i = 0; i < numsSize; i++)
148
149从头扫一遍。
150
151但是不能重复用同一个数,所以用:used[i]或者用bool判断你是否选择过
152
153int** permute(int* nums, int n, int* returnSize, int** returnColumnSizes) {
154 // 计算 n!
155 int ansSize = 1;
156 for (int i = 2; i <= n; i++) {
157 ansSize *= i;
158 }
159
160 int** ans = malloc(ansSize * sizeof(int*));
161 *returnColumnSizes = malloc(ansSize * sizeof(int));
162 *returnSize = 0;
163
164 int* path = malloc(n * sizeof(int));
165 bool* on_path = calloc(n, sizeof(bool)); // 所有排列的长度都是一样的 n
166
167 // 枚举 path[i] 填什么数字
168 void dfs(int i) {
169 if (i == n) {
170 ans[*returnSize] = malloc(n * sizeof(int));
171 memcpy(ans[*returnSize], path, n * sizeof(int));
172 (*returnColumnSizes)[*returnSize] = n;
173 (*returnSize)++;
174 return;
175 }
176
177 for (int j = 0; j < n; j++) {
178 if (!on_path[j]) {
179 path[i] = nums[j]; // 从没有选的数字中选一个
180 on_path[j] = true; // 已选上
181 dfs(i + 1);
182 on_path[j] = false; // 恢复现场
183 // 注意 path 无需恢复现场,因为排列长度固定,直接覆盖就行
184 }
185 }
186 }
187
188 dfs(0);
189
190 free(path);
191 free(on_path);
192 return ans;
193}
194
195它分成 4 块:
196
1971. 先算一共有多少个答案
1982. 给答案数组 ans 分配空间
1993. 准备 path 和 on_path
2004. dfs 填排列
2011. 计算一共有多少个排列
202
203int ansSize = 1;
204for (int i = 2; i <= n; i++) {
205 ansSize *= i;
206}
207
208如果 n = 3:
209
210ansSize = 1 × 2 × 3 = 6
211
212因为 [1,2,3] 一共有:
213
2143! = 6
215
216个排列。
217
2182. 给答案分配空间
219
220int** ans = malloc(ansSize * sizeof(int*));
221
222ans 是二维数组。
223
224可以理解成:
225
226ans[0] -> 一个排列
227ans[1] -> 一个排列
228ans[2] -> 一个排列
229...
230
231比如:
232
233ans[0] = [1,2,3]
234ans[1] = [1,3,2]
235ans[2] = [2,1,3]
236
237所以 ans 本身是:int**
238
239*returnColumnSizes = malloc(ansSize * sizeof(int));
240
241这个是 LeetCode 要求的。
242
243它记录每一行有几个元素。
244
245因为全排列每一行长度都是 n,比如:
246
247[1,2,3] 长度 3
248[1,3,2] 长度 3
249[2,1,3] 长度 3
250
251所以后面每一行都填 n。
252
253*returnSize = 0;
254
255表示:目前已经保存了 0 个排列
256
257以后每找到一个排列,就:
258
259(*returnSize)++;
260
2613. 准备 path 和 on_path
262int* path = malloc(n * sizeof(int));
263
264path 是当前正在填的排列。
265
266比如搜索过程中:
267
268path = [1, _, _]
269path = [1, 2, _]
270path = [1, 2, 3]
271bool* on_path = calloc(n, sizeof(bool));
272
273on_path[j] 表示:
274
275nums[j] 这个数有没有被用过
276
277如果:
278
279nums = [1,2,3]
280
281一开始:
282
283on_path = [false, false, false]
284
285选了 1 之后:
286
287on_path = [true, false, false]
288
289表示 nums[0] = 1 已经用过了。
290
2914. dfs 的含义
292void dfs(int i)
293
294这里的 i 表示:
295
296现在正在填 path[i]
297
298比如:
299
300dfs(0):填 path[0]
301dfs(1):填 path[1]
302dfs(2):填 path[2]
303dfs(3):说明 path[0], path[1], path[2] 都填完了
304
3055. 结束条件
306if (i == n) {
307
308如果 n = 3,当 i == 3 时,说明:
309
310path[0], path[1], path[2]
311
312都已经填好了。
313
314比如:
315
316path = [1,2,3]
317
318这就是一个完整排列。
319
320保存答案:
321
322ans[*returnSize] = malloc(n * sizeof(int));
323
324给当前这一行分配空间。
325
326比如:
327
328ans[0] 准备存 [1,2,3]
329memcpy(ans[*returnSize], path, n * sizeof(int));
330
331把 path 复制到 ans[*returnSize]。
332
333memcpy 理解成:
334
335把 path 里的 n 个 int 复制到 ans 当前这一行
336
337等价于手写:
338
339for(int k = 0; k < n; k++) {
340 ans[*returnSize][k] = path[k];
341}
342(*returnColumnSizes)[*returnSize] = n;
343
344告诉 LeetCode:
345
346这一行有 n 个数
347(*returnSize)++;
348
349表示:
350
351答案数量 +1
352
3536. for 循环:当前位置填谁
354for (int j = 0; j < n; j++) {
355
356意思是:
357
358我现在要填 path[i]
359尝试用 nums[0], nums[1], nums[2]...
360if (!on_path[j]) {
361
362如果 nums[j] 没用过,就可以选。
363
364path[i] = nums[j];
365
366把这个数填到当前位置。
367
368比如:
369
370i = 0, j = 0
371path[0] = nums[0] = 1
372
373得到:
374
375path = [1, _, _]
376on_path[j] = true;
377
378标记:
379
380nums[j] 已经用过
381dfs(i + 1);
382
383去填下一个位置。
384
385如果现在填完 path[0],下一步就填:
386
387path[1]
388on_path[j] = false;
389
390恢复现场。
391
392意思是:
393
394刚刚试过 nums[j] 了
395现在退回来
396让 nums[j] 可以给别的排列继续使用
397用 [1,2,3] 看一次
398
399开始:
400
401dfs(0)
402path = [_,_,_]
403on_path = [F,F,F]
404
405选 1:
406
407path = [1,_,_]
408on_path = [T,F,F]
409dfs(1)
410
411选 2:
412
413path = [1,2,_]
414on_path = [T,T,F]
415dfs(2)
416
417选 3:
418
419path = [1,2,3]
420on_path = [T,T,T]
421dfs(3)
422
423i == n,保存:
424
425[1,2,3]
426
427然后回溯,撤销 3:
428
429on_path = [T,T,F]
430
431再回去撤销 2,尝试选 3:
432
433path = [1,3,_]
434on_path = [T,F,T]
435
436再选 2:
437
438path = [1,3,2]
439
440保存。
441
442最核心三句
443
444path[i] = nums[j];
445on_path[j] = true;
446dfs(i + 1);
447on_path[j] = false;
448
449意思就是:
450
451把 nums[j] 填到第 i 个位置
452标记它已经用过
453递归去填下一个位置
454回来后撤销标记,换别的数试
455
456path 不用恢复,因为下一次会直接覆盖 path[i]。
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
4916. 易错点
4921. 排列型不能用 start,否则会漏掉 [2,1] 这种顺序。
493
4942. 每一层都从 i=0 开始枚举。
495
4963. 必须 used[i] 防止重复使用同一个数。
497
4984. 回溯时既要 pathSize--,也要 used[i]=0。
499
5005. 保存答案时要复制 path,不能直接保存 path 指针。
501
502
503排列型回溯 = 每个位置枚举一个没用过的数。
Rendered Preview

看一下灵神的代码
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
path = [0] * n
ans = []

    # 枚举 path[i] 填 remain(剩余数字)中的哪个数
    def dfs(i: int, remain: Set[int]) -> None:
        if i == n:
            ans.append(path.copy())
            return

        for x in remain:
            path[i] = x
            dfs(i + 1, remain - {x})

    dfs(0, set(nums))
    return ans

用bool判断用过没用过就是下面这种
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
path = [0] * n # 所有排列的长度都是一样的 n
on_path = [False] * n
ans = []

    # 枚举 path[i] 填 nums 的哪个数
    def dfs(i: int) -> None:
        if i == n:
            ans.append(path.copy())  # 也可以写 path[:]
            return
        for j, on in enumerate(on_path):
            if not on:
                path[i] = nums[j]  # 从没有选的数字中选一个
                on_path[j] = True  # 已选上
                dfs(i + 1)
                on_path[j] = False  # 恢复现场
                # 注意 path 无需恢复现场,因为排列长度固定,直接覆盖就行

    dfs(0)
    return ans

复杂度分析
时间复杂度:O(n⋅n!),其中 n 为 nums 的长度。视频中提到,搜索树中的节点个数低于 3⋅n!。实际上,精确值为 ⌊e⋅n!⌋,其中 e=2.718⋯ 为自然常数。有 O(n!) 个叶节点,每个叶节点花费 O(n) 的时间复制 path 数组,因此时间复杂度为 O(n⋅n!)。
空间复杂度:O(n)。返回值的空间不计入。

排列型回溯:46 全排列

  1. 前提条件

全排列问的是:

给 nums 里的所有数,重新排列出所有顺序。

例如:

nums = [1,2,3]

答案:

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

核心和组合不同:

组合:不能回头选,靠 start
排列:每一层都可以从头选,靠 used[]

  1. 核心概念

排列型每一层是在问:

当前位置放谁?

比如 [1,2,3]:

第 0 个位置:可以放 1/2/3
第 1 个位置:放剩下没用过的
第 2 个位置:放最后一个

所以需要:int used[MAXN];

表示:

这个数有没有被当前 path 用过

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

    for(int i = 0; i < numsSize; i++)
    {
    if(used[i])
    {
    continue;
    }

     used[i] = 1;
     path[pathSize++] = nums[i];
    
     dfs();
    
     pathSize--;
     used[i] = 0;
    

    }
    }

核心还是:

选择
递归
撤销

只不过排列型撤销两个东西:

pathSize--
used[i] = 0

  1. 为什么不用 start?

组合型:

[1,2] 和 [2,1] 算同一个

所以用:

dfs(i + 1);

防止回头。

排列型:

[1,2] 和 [2,1] 是两个不同答案

所以每一层都要:

for(int i = 0; i < numsSize; i++)

从头扫一遍。

但是不能重复用同一个数,所以用:used[i]或者用bool判断你是否选择过

int** permute(int* nums, int n, int* returnSize, int** returnColumnSizes) {
// 计算 n!
int ansSize = 1;
for (int i = 2; i <= n; i++) {
ansSize *= i;
}

int** ans = malloc(ansSize * sizeof(int*));
*returnColumnSizes = malloc(ansSize * sizeof(int));
*returnSize = 0;

int* path = malloc(n * sizeof(int));
bool* on_path = calloc(n, sizeof(bool)); // 所有排列的长度都是一样的 n

// 枚举 path[i] 填什么数字
void dfs(int i) {
    if (i == n) {
        ans[*returnSize] = malloc(n * sizeof(int));
        memcpy(ans[*returnSize], path, n * sizeof(int));
        (*returnColumnSizes)[*returnSize] = n;
        (*returnSize)++;
        return;
    }

    for (int j = 0; j < n; j++) {
        if (!on_path[j]) {
            path[i] = nums[j]; // 从没有选的数字中选一个
            on_path[j] = true; // 已选上
            dfs(i + 1);
            on_path[j] = false; // 恢复现场
            // 注意 path 无需恢复现场,因为排列长度固定,直接覆盖就行
        }
    }
}

dfs(0);

free(path);
free(on_path);
return ans;

}

它分成 4 块:

  1. 先算一共有多少个答案
  2. 给答案数组 ans 分配空间
  3. 准备 path 和 on_path
  4. dfs 填排列
  5. 计算一共有多少个排列

int ansSize = 1;
for (int i = 2; i <= n; i++) {
ansSize *= i;
}

如果 n = 3:

ansSize = 1 × 2 × 3 = 6

因为 [1,2,3] 一共有:

3! = 6

个排列。

  1. 给答案分配空间

int** ans = malloc(ansSize * sizeof(int*));

ans 是二维数组。

可以理解成:

ans[0] -> 一个排列
ans[1] -> 一个排列
ans[2] -> 一个排列
...

比如:

ans[0] = [1,2,3]
ans[1] = [1,3,2]
ans[2] = [2,1,3]

所以 ans 本身是:int**

*returnColumnSizes = malloc(ansSize * sizeof(int));

这个是 LeetCode 要求的。

它记录每一行有几个元素。

因为全排列每一行长度都是 n,比如:

[1,2,3] 长度 3
[1,3,2] 长度 3
[2,1,3] 长度 3

所以后面每一行都填 n。

*returnSize = 0;

表示:目前已经保存了 0 个排列

以后每找到一个排列,就:

(*returnSize)++;

  1. 准备 path 和 on_path
    int* path = malloc(n * sizeof(int));

path 是当前正在填的排列。

比如搜索过程中:

path = [1, _, _]
path = [1, 2, _]
path = [1, 2, 3]
bool* on_path = calloc(n, sizeof(bool));

on_path[j] 表示:

nums[j] 这个数有没有被用过

如果:

nums = [1,2,3]

一开始:

on_path = [false, false, false]

选了 1 之后:

on_path = [true, false, false]

表示 nums[0] = 1 已经用过了。

  1. dfs 的含义
    void dfs(int i)

这里的 i 表示:

现在正在填 path[i]

比如:

dfs(0):填 path[0]
dfs(1):填 path[1]
dfs(2):填 path[2]
dfs(3):说明 path[0], path[1], path[2] 都填完了

  1. 结束条件
    if (i == n) {

如果 n = 3,当 i == 3 时,说明:

path[0], path[1], path[2]

都已经填好了。

比如:

path = [1,2,3]

这就是一个完整排列。

保存答案:

ans[*returnSize] = malloc(n * sizeof(int));

给当前这一行分配空间。

比如:

ans[0] 准备存 [1,2,3]
memcpy(ans[*returnSize], path, n * sizeof(int));

把 path 复制到 ans[*returnSize]。

memcpy 理解成:

把 path 里的 n 个 int 复制到 ans 当前这一行

等价于手写:

for(int k = 0; k < n; k++) {
ans[*returnSize][k] = path[k];
}
(*returnColumnSizes)[*returnSize] = n;

告诉 LeetCode:

这一行有 n 个数
(*returnSize)++;

表示:

答案数量 +1

  1. for 循环:当前位置填谁
    for (int j = 0; j < n; j++) {

意思是:

我现在要填 path[i]
尝试用 nums[0], nums[1], nums[2]...
if (!on_path[j]) {

如果 nums[j] 没用过,就可以选。

path[i] = nums[j];

把这个数填到当前位置。

比如:

i = 0, j = 0
path[0] = nums[0] = 1

得到:

path = [1, _, _]
on_path[j] = true;

标记:

nums[j] 已经用过
dfs(i + 1);

去填下一个位置。

如果现在填完 path[0],下一步就填:

path[1]
on_path[j] = false;

恢复现场。

意思是:

刚刚试过 nums[j] 了
现在退回来
让 nums[j] 可以给别的排列继续使用
用 [1,2,3] 看一次

开始:

dfs(0)
path = [,,_]
on_path = [F,F,F]

选 1:

path = [1,,]
on_path = [T,F,F]
dfs(1)

选 2:

path = [1,2,_]
on_path = [T,T,F]
dfs(2)

选 3:

path = [1,2,3]
on_path = [T,T,T]
dfs(3)

i == n,保存:

[1,2,3]

然后回溯,撤销 3:

on_path = [T,T,F]

再回去撤销 2,尝试选 3:

path = [1,3,_]
on_path = [T,F,T]

再选 2:

path = [1,3,2]

保存。

最核心三句

path[i] = nums[j];
on_path[j] = true;
dfs(i + 1);
on_path[j] = false;

意思就是:

把 nums[j] 填到第 i 个位置
标记它已经用过
递归去填下一个位置
回来后撤销标记,换别的数试

path 不用恢复,因为下一次会直接覆盖 path[i]。

  1. 易错点

  2. 排列型不能用 start,否则会漏掉 [2,1] 这种顺序。

  3. 每一层都从 i=0 开始枚举。

  4. 必须 used[i] 防止重复使用同一个数。

  5. 回溯时既要 pathSize--,也要 used[i]=0。

  6. 保存答案时要复制 path,不能直接保存 path 指针。

排列型回溯 = 每个位置枚举一个没用过的数。