Skip to content

Commit 89c4348

Browse files
committed
调整回溯内容
1 parent ff0e5c2 commit 89c4348

File tree

3 files changed

+56
-55
lines changed

3 files changed

+56
-55
lines changed

Diff for: docs/0000-24-backtrack.adoc

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,51 @@
11
[#0000-24-backtrack]
22
= Backtracking 回溯
33

4-
参考 xref:0046-permutations.adoc[46. Permutations] 认真学习一下回溯思想。
4+
首先介绍“回溯”算法的应用。“回溯”算法也叫“回溯搜索”算法,主要用于在一个庞大的空间里搜索我们所需要的问题的解。我们每天使用的“搜索引擎”就是帮助我们在庞大的互联网上搜索我们需要的信息。“搜索”引擎的“搜索”和“回溯搜索”算法的“搜索”意思是一样的。
5+
6+
“回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”,是在编码的过程中,是为了节约空间而使用的一种技巧。而回溯其实是“深度优先遍历”特有的一种现象。之所以是“深度优先遍历”,是因为我们要解决的问题通常是在一棵树上完成的,在这棵树上搜索需要的答案,一般使用深度优先遍历。
7+
8+
“全排列”就是一个非常经典的“回溯”算法的应用。我们知道,N 个数字的全排列一共有 N! 这么多个。
9+
10+
使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是**执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。**
11+
12+
image::images/0046-01.png[{image_attr}]
13+
14+
说明:
15+
16+
. 每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现;
17+
. 这些变量的不同的值,也称之为“状态”;
18+
. 使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样;
19+
. 因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”;
20+
. 深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,`path` 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 `path` 变量是一个栈。
21+
. 深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。
22+
23+
24+
**解决一个回溯问题,实际上就是一个决策树的遍历过程。**只需要思考 3 个问题:
25+
26+
. 路径:也就是已经做出的选择。
27+
. 选择列表:也就是你当前可以做的选择。
28+
. 结束条件:也就是到达决策树底层,无法再做选择的条件。
29+
30+
代码方面,回溯算法的框架:
31+
32+
[source]
33+
----
34+
result = []
35+
def backtrack(路径, 选择列表):
36+
if 满足结束条件:
37+
result.add(路径)
38+
return
39+
40+
for 选择 in 选择列表:
41+
做选择
42+
backtrack(路径, 选择列表)
43+
撤销选择
44+
----
45+
46+
**其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」**,特别简单。
47+
48+
必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。**
549

650
玩回溯,一定要画出递归调用树。
751

Diff for: docs/0046-permutations.adoc

-47
Original file line numberDiff line numberDiff line change
@@ -34,55 +34,8 @@ https://leetcode.cn/problems/permutations/[LeetCode - 46. 全排列 ^]
3434
3535
== 思路分析
3636

37-
首先介绍“回溯”算法的应用。“回溯”算法也叫“回溯搜索”算法,主要用于在一个庞大的空间里搜索我们所需要的问题的解。我们每天使用的“搜索引擎”就是帮助我们在庞大的互联网上搜索我们需要的信息。“搜索”引擎的“搜索”和“回溯搜索”算法的“搜索”意思是一样的。
38-
39-
“回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”,是在编码的过程中,是为了节约空间而使用的一种技巧。而回溯其实是“深度优先遍历”特有的一种现象。之所以是“深度优先遍历”,是因为我们要解决的问题通常是在一棵树上完成的,在这棵树上搜索需要的答案,一般使用深度优先遍历。
40-
41-
“全排列”就是一个非常经典的“回溯”算法的应用。我们知道,N 个数字的全排列一共有 N! 这么多个。
42-
43-
使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是**执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。**
44-
45-
image::images/0046-01.png[{image_attr}]
46-
4737
image::images/0046-02.png[{image_attr}]
4838

49-
说明:
50-
51-
. 每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现;
52-
. 这些变量的不同的值,也称之为“状态”;
53-
. 使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样;
54-
. 因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”;
55-
. 深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈。
56-
. 深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。
57-
58-
59-
**解决一个回溯问题,实际上就是一个决策树的遍历过程。**只需要思考 3 个问题:
60-
61-
. 路径:也就是已经做出的选择。
62-
. 选择列表:也就是你当前可以做的选择。
63-
. 结束条件:也就是到达决策树底层,无法再做选择的条件。
64-
65-
代码方面,回溯算法的框架:
66-
67-
[source]
68-
----
69-
result = []
70-
def backtrack(路径, 选择列表):
71-
if 满足结束条件:
72-
result.add(路径)
73-
return
74-
75-
for 选择 in 选择列表:
76-
做选择
77-
backtrack(路径, 选择列表)
78-
撤销选择
79-
----
80-
81-
**其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」**,特别简单。
82-
83-
必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。**这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。**
84-
85-
8639
[[src-0046]]
8740
[tabs]
8841
====

Diff for: src/main/java/com/diguage/algo/leetcode/_0046_Permutations_4.java

+11-7
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,31 @@ public class _0046_Permutations_4 {
77
// tag::answer[]
88
/**
99
* @author D瓜哥 · https://www.diguage.com
10-
* @since 2024-09-18 22:11:50
10+
* @since 2025-04-06 16:50
1111
*/
1212
public List<List<Integer>> permute(int[] nums) {
1313
List<List<Integer>> result = new ArrayList<>();
1414
backtrack(nums, result, new ArrayList<>(), new boolean[nums.length]);
1515
return result;
1616
}
1717

18-
private void backtrack(int[] nums, List<List<Integer>> result, List<Integer> added, boolean[] used) {
19-
if (added.size() == nums.length) {
20-
result.add(new ArrayList<>(added));
18+
private void backtrack(int[] nums, List<List<Integer>> result,
19+
List<Integer> path, boolean[] used) {
20+
if (path.size() == nums.length) {
21+
result.add(new ArrayList<>(path));
22+
return;
2123
}
2224
for (int i = 0; i < nums.length; i++) {
2325
if (used[i]) {
2426
continue;
2527
}
28+
// 选择
2629
used[i] = true;
27-
added.add(nums[i]);
28-
backtrack(nums, result, added, used);
30+
path.add(nums[i]);
31+
backtrack(nums, result, path, used);
32+
// 撤销
33+
path.removeLast();
2934
used[i] = false;
30-
added.remove(added.size() - 1);
3135
}
3236
}
3337
// end::answer[]

0 commit comments

Comments
 (0)