|
1 | 1 |
|
2 | | -> |
| 2 | +>今天开始正式回溯法的讲解,老规矩,先概述 |
3 | 3 |
|
4 | 4 | #什么是回溯法 |
| 5 | + |
5 | 6 | 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 |
6 | 7 |
|
7 | 8 | 在二叉树系列中,我们已经不止一次,提到了回溯,例如[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)。 |
8 | 9 |
|
9 | 10 | 回溯是递归的副产品,只要有递归就会有回溯。 |
10 | 11 |
|
11 | | -回溯法的性能如何呢,这里要很大家说清楚了,**虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法**。 |
| 12 | +**所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数**。 |
12 | 13 |
|
13 | 14 | #回溯法的效率 |
14 | 15 |
|
| 16 | +回溯法的性能如何呢,这里要和大家说清楚了,**虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法**。 |
| 17 | + |
15 | 18 | **因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案**,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。 |
16 | 19 |
|
17 | 20 | 那么既然回溯法并不高效为什么还要用它呢? |
18 | 21 |
|
19 | | -因为没得选,一些问题只能暴力搜,没有更优的算法了。 |
| 22 | +因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。 |
20 | 23 |
|
21 | | -#回溯法解决的问题 |
| 24 | +此时大家应该好奇了,都什么问题,这么牛逼,只能暴力搜索。 |
22 | 25 |
|
| 26 | +#回溯法解决的问题 |
23 | 27 |
|
| 28 | +回溯法,一般可以解决如下几种问题: |
24 | 29 |
|
25 | | -回溯算法,相信 |
| 30 | +* 组合问题:N个数里面按一定规则找出k个数的集合 |
| 31 | +* 排列问题:N个数按一定规则全排列,有几种排列方式 |
| 32 | +* 切割问题:一个字符串按一定规则有几种切割方式 |
| 33 | +* 子集问题:一个N个数的集合里有多少符合条件的子集 |
| 34 | +* 棋盘问题:N皇后,解数独等等 |
26 | 35 |
|
27 | | -**回溯的问题都可以抽象为一个树形结构,在求解组合问题的过程中,n相当于树的宽度,k相当于树的深度**。 |
| 36 | +**相信大家看着这些之后会发现,每个问题,都不简单!** |
28 | 37 |
|
29 | | -#题外话 |
30 | 38 |
|
31 | | -一些同学可能分不清什么是组合,什么是排列? |
| 39 | +另外,会有一些同学可能分不清什么是组合,什么是排列? |
32 | 40 |
|
33 | | -组合是不强调元素顺序的,排列是强调元素顺序的。 |
| 41 | +**组合是不强调元素顺序的,排列是强调元素顺序**。 |
34 | 42 |
|
35 | 43 | 例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。 |
36 | 44 |
|
37 | | -##回溯算法模板 |
| 45 | +记住组合无序,排列有序,就可以了。 |
38 | 46 |
|
39 | | -这里将给出Carl总结的回溯算法模板,那么先来讲一讲回溯算法的模板伪代码。 |
| 47 | +#如何理解回溯法 |
40 | 48 |
|
41 | | -既然回溯是递归的副产品,那么必然要有递归。 |
| 49 | +**回溯法解决的问题都可以抽象为树形结构**,是的,我指的是所有回溯法的问题都可以抽象为树形结构! |
| 50 | + |
| 51 | +因为回溯法解决的都是在集合中递归查找子集,**集合的大小就构成了树的宽度,递归的深度,都构成的树的深度**。 |
| 52 | + |
| 53 | +递归就要有终止条件,所以必然是一颗高度有限的树(N叉树)。 |
| 54 | + |
| 55 | +这块可能初学者还不太理解,后面的回溯算法解决的所有题目中,我都会强调这一点并画图举相应的例子,现在有一个印象就行。 |
42 | 56 |
|
43 | | -**所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数**。 |
| 57 | + |
| 58 | +#回溯法模板 |
| 59 | + |
| 60 | +这里给出Carl总结的回溯算法模板。 |
44 | 61 |
|
45 | 62 | 在讲[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)中我们说了递归三部曲,这里我再给大家列出回溯三部曲。 |
46 | 63 |
|
47 | 64 | * 回溯函数模板返回值以及参数 |
48 | 65 |
|
| 66 | +在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。 |
| 67 | + |
| 68 | +回溯算法中函数返回值一般为void。 |
| 69 | + |
| 70 | +再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。 |
| 71 | + |
| 72 | +但后面的回溯题目的讲解中,为了方便大家理解,我在一开始就帮大家把参数确定下来。 |
| 73 | + |
49 | 74 | 回溯函数伪代码如下: |
50 | 75 |
|
51 | 76 | ``` |
52 | | -backtracking(参数) |
| 77 | +voidbacktracking(参数) |
53 | 78 | ``` |
54 | 79 |
|
55 | | -在回溯算法中,我的习惯函数起名字为backtracking,回溯中递归返回值一般为void。 |
| 80 | +* 回溯函数终止条件 |
56 | 81 |
|
57 | | -在来看一下参数,因为回溯算法需要的参数,可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,需要什么参数,就填什么参数。 |
| 82 | +既然是树形结构,那么我们在讲解[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)的时候,就知道遍历树形结构一定要有终止条件。 |
58 | 83 |
|
59 | | -但后面的回溯题目的讲解中,为了方便理解,我在一开始就帮大家把参数确定下来。 |
| 84 | +所以回溯也有要终止条件。 |
60 | 85 |
|
61 | | -* 回溯函数终止条件 |
| 86 | +什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。 |
62 | 87 |
|
63 | | -回溯函数终止条件伪代码如下: |
| 88 | +所以回溯函数终止条件伪代码如下: |
64 | 89 | ``` |
65 | 90 | if (终止条件) { |
66 | 91 | 存放结果; |
| 92 | + return; |
67 | 93 | } |
68 | 94 | ``` |
69 | 95 |
|
70 | | -既然是树形结构,那么我们在讲解[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)的时候,就知道遍历树形结构一定要有终止条件。 |
| 96 | +* 回溯搜索的遍历过程 |
71 | 97 |
|
72 | | -所以回溯也有要终止条件。 |
| 98 | +在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。 |
73 | 99 |
|
74 | | -什么时候达到了终止条件,树中就可以看出,搜到了叶子节点了,就找到了一个符合题目要求的答案,就把这个答案存放起来。 |
| 100 | +如图: |
75 | 101 |
|
| 102 | +<imgsrc='../pics/回溯算法理论基础.png'width=600> </img></div> |
76 | 103 |
|
77 | | -* 回溯搜索的遍历过程 |
| 104 | +注意图中,我特意举例集合大小和孩子的数量是相等的! |
78 | 105 |
|
79 | 106 | 回溯函数遍历过程伪代码如下: |
80 | 107 | ``` |
81 | | -for (选择:选择列表(可以想成树中每个节点孩子的数量)) { |
82 | | -递归,处理节点; |
83 | | - backtracking(路径,选择列表); |
| 108 | +for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { |
| 109 | + 处理节点; |
| 110 | + backtracking(路径,选择列表); // 递归 |
84 | 111 | 回溯,撤销处理结果 |
85 | 112 | } |
86 | 113 | ``` |
87 | 114 |
|
88 | | -看一下这个for循环,这个for循环是做什么呢? |
89 | 115 |
|
90 | | -for 就是处理树中每一个节点的各个孩子的情况, 一个节点有多少个孩子,这个for循环就执行多少次。 |
| 116 | +for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。 |
91 | 117 |
|
92 | | -注意这个backtracking就是自己调用自己,实现递归。 |
| 118 | +backtracking这里自己调用自己,实现递归。 |
93 | 119 |
|
94 | | -**一些同学对递归操作本来就不熟练,递归上面又加上一个for循环,可能就更迷糊了**, 我再给大家捋顺一下。 |
95 | | - |
96 | | -这个backtracking(递归函数)是从根节点向树的叶子节点方向遍历,**for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历**,这样就把这棵树全遍历完了,如图所示: |
97 | | - |
98 | | -<imgsrc='../pics/77.组合1.png'width=600> </img></div> |
99 | | - |
100 | | -backtracking一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回,那么backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。 |
| 120 | +大家可以从图中看出**for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历**,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。 |
101 | 121 |
|
102 | 122 | 分析完过程,回溯算法模板框架如下: |
103 | 123 |
|
104 | 124 | ``` |
105 | | -backtracking(参数) { |
| 125 | +voidbacktracking(参数) { |
106 | 126 | if (终止条件) { |
107 | 127 | 存放结果; |
| 128 | + return; |
108 | 129 | } |
109 | 130 |
|
110 | | - for (选择:选择列表(可以想成树中每个节点孩子的数量)) { |
111 | | -递归,处理节点; |
112 | | - backtracking(路径,选择列表); |
| 131 | + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { |
| 132 | + 处理节点; |
| 133 | + backtracking(路径,选择列表); // 递归 |
113 | 134 | 回溯,撤销处理结果 |
114 | 135 | } |
115 | 136 | } |
116 | 137 |
|
117 | 138 | ``` |
118 | 139 |
|
| 140 | +**这份模板很重要,后面做回溯法的题目都靠它了!** |
| 141 | + |
| 142 | +如果从来没有学过回溯算法的录友们,看到这里会有点懵,后面开始讲解具体题目的时候就会好一些了,已经做过回溯法题目的录友,看到这里应该会感同身受了。 |
| 143 | + |
| 144 | +#总结 |
| 145 | + |
| 146 | +本篇我们讲解了,什么是回溯算法,知道了回溯和递归是相辅相成的。 |
| 147 | + |
| 148 | +接着提到了回溯法的效率,回溯法其实就是暴力查找,并不是什么高效的算法。 |
| 149 | + |
| 150 | +然后列出了回溯法可以解决几类问题,可以看出每一类问题都不简单。 |
| 151 | + |
| 152 | +最后我们讲到回溯法解决的问题都可以抽象为树形结构(N叉树),并给出了回溯法的模板。 |
| 153 | + |
| 154 | +今天是回溯算法的第一天,按照惯例Carl都是先概述一波,然后在开始讲解具体题目,没有接触过回溯法的同学刚学起来有点看不懂很正常,后面和具体题目结合起来会好一些。 |
| 155 | + |
| 156 | + |
| 157 | + |