- 这是《算法笔记》的读书记录
- 本文参考自8.1节
文章目录
- 一、引子
- 二、深度优先搜索DFS
- 三、DFS和回溯
- 四、例题
- 1. 搜索二叉树
- 2. 背包问题
一、引子
- 现在我们身处一个迷宫之中,可能很多人都听说过这个说法:只要每个岔路都走右手边的分支,就一定能走出去。下图是一个示例,从起点开始,每个分岔都走右边,最后找到了出口
- 这里其实就执行了深度优先搜索算法,我们可以这样理解:在岔路位置,如果不拐弯走直线,就看作维持在当前层面;如果拐弯了,就是走到迷宫更深处了。在每个岔路我们总是选择拐弯,总是以“深度”作为前进的关键,不碰到死胡同就不回头,因此称这种方式为深度优先搜索。
二、深度优先搜索DFS
- 定义:对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次
- 深搜可以用栈实现:以引子的走迷宫为例
- ABD入栈
- H入栈,发现是死胡同,出栈回到D;类似的,I,J也先入栈再出栈回到D
- D后面都是死路,D出栈回到B
- E入栈
- …
- 虽然可以用栈实现,但是具体写的时候会很麻烦,所以通常使用递归实现DFS
- 递归式 = 岔道口
- 递归边界 = 死胡同
使用递归的时候,因为有函数的反复调用,编译器处理的时候会在内存中使用函数调用栈来存储每一层的状态,所以本质上还是栈实现
- 下面是一个递归计算斐波那契数列的例子,递归式为
f(n) = f(n-1) + f(n-2)
递归树如下
递归进行时,f(n-1)
和f(n-2)
就相当于两个岔路口,我们一直沿f(n-1)
这条路走到递归边界(死胡同),再回到上一层走f(n-2)
,可见这里也蕴含着DFS的思想
三、DFS和回溯
- DFS和回溯法很相似,主要区别在于:
- DFS是对搜索树的搜索过程,标准的DFS要走过整个树(实际是穷举了);回溯法一般要做剪枝。
- DFS不用保存记录访问过的状态(一般用全局变量),常会定义一个全局的结果,在每次满足递归结束条件时(叶子)刷新计算结果,这样最后只有一个输出;回溯法通常要保存访问过的状态(比如存一下走过的路线),回溯返回时要恢复标志(恢复标记正是回溯名词的由来)。
- 现在也常常对DFS进行剪枝和记录状态,这种处理方法使得深度优先搜索法与回溯法没什么区别了,具体可以看下面 四.2 部分的背包题例子
- DFS的一般模板
- 回溯法的一般模板
四、例题
1. 搜索二叉树
- 考虑DFS搜索一颗完全二叉树,从根节点开始,每一层看成在当前位置做一个二分选择,选左子树为0,右子树找为1,显示找到叶子时的所有路线
- 输出
- 分析输出:第一条路线是一直走左边,第二条路线是最后一个岔路走右边,然后回到倒数第二层走右边…可以看出这是一个DFS的路线
2. 背包问题
- 一共有N件物品,每件重w[i],价值c[i]。现在要选出若干物品放入一个容量为V的背包,使得在选入背包的物品重量和不超过容量V的前提下,使包中物品价值最高,求最大价值
- 分析
- “岔道口”(递归式):要不要把第i件物品放入背包
- “死胡同”(递归边界):选择物品的质量超过V
- 示例代码如下
- 输出结果
- 可见,因为每种物品都有放入和不放入两种选择,而上述代码总是先找出所有可能的放置序列,再判断序列是否满足要求,因此n件物品的时间复杂度为。这其实是一种 “暴力” 法。如果再去除记录选择的
select
数组,就完全成为一个标准的DFS - 利用背包容量这个限制条件,我们可以提前禁止某些分支。把上面代码
if(sumW+w[i]<=V)
这个注释取消,输出如下,可见限制过后所有输出一定是满足条件的了。这是一种 “剪枝” 方法,去除了递归树中的一些分支,提高了效率。也可也说这是回溯法
- 事实上,这个例子给出了一类常见DFS问题的解法,即给定一个序列,枚举这个序列的所有子集序列,从中选择一个 “最优” 子序列。