算法解释
深度优先搜索和广度优先搜索是两种最常见的优先搜索方法,它们被广泛地运用在图和树等结构中进行搜索。
深度优先搜索
深度优先搜索 (DFS) 在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。对于树结构而言,由于总是对新节点调用遍历,因此看起来是向着“深”的方向前进。
深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用之后会讲到的拓扑排序判断是否有环路,若最后存在入度不为零的点,则说明有环。
有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化 (Memoization)。
695. Max Area of Island
一般来说,深度优先搜索类型的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。
547. Number of Provinces
本题拥有 n 个节点,每个节点最多有 n 条边,表示和所有城市在同一省份,最少可以有 1 条边,表示是孤立的城市。当清楚了图的表示方法后,这道题与上一道题本质相同:搜索城市圈(岛屿圈)的个数。
对于节点连接类问题,我们也可以利用并查集来进行快速的连接和搜索。
417. Pacific Atlantic Water Flow
虽然题目要求的是满足向下流能到达两个大洋的位置,如果我们对所有的位置进行搜索,那么在不剪枝的情况下复杂度会很高。因此我们可以反过来想,从两个大洋开始向上流,这样我们只需要对矩形四条边进行搜索。
回溯法
回溯法 (Backtracking) 是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。
顾名思义,回溯法的核心是回溯。在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。这样的好处是我们可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。在具体的写法上,它与普通的深度优先搜索一样,都有 [修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了 [修改当前节点状态]→[递归子节点]→[回改当前节点状态]。
没有接触过回溯法的读者可能会不明白我在讲什么,这也完全正常,希望以下几道题可以让您理解回溯法。如果还是不明白,可以记住两个小诀窍:
- 按引用传状态。
- 所有的状态修改在递归完成后回改。一般有两种情况:修改最后一位输出,比如排列组合;修改访问标记,比如矩阵里搜字符串。
46. Permutations
怎样输出所有的排列方式呢?对于每一个当前位置 i,我们可以将其于之后的任意位置交换,然后继续处理位置 i+1,直到处理到最后一位。
77. Combinations
类似于排列问题,我们也可以进行回溯。排列回溯的是交换的位置,而组合回溯的是是否把当前的数字加入结果中。
79. Word Search
不同于排列组合问题,本题采用的并不是修改输出方式,而是修改访问标记。
51. N-Queens
类似于在矩阵中寻找字符串,本题也是通过修改状态矩阵来进行回溯。
广度优先搜索
广度优先搜索 (BFS) 不同与深度优先搜索,它是一层层进行遍历的,因此需要用先入先出的队列而非先入后出的栈进行遍历。由于是按层次进行遍历,广度优先搜索时按照“广”的方向进行遍历的,也常常用来处理最短路径等问题。
这里要注意,深度优先搜索和广度优先搜索都可以处理可达性问题,即从一个节点开始是否能达到另一个节点。因为深度优先搜索可以利用递归快速实现,很多人会习惯使用深度优先搜索刷此类题目。实际软件工程中,笔者很少见到递归的写法,因为一方面难以理解,另一方面可能产生栈溢出的情况;而用栈实现的深度优先搜索和用队列实现的广度优先搜索在写法上并没有太大差异,因此使用哪一种搜索方式需要根据实际的功能需求来判断。另外,如果需要自定义搜索优先级,我们可以利用优先队列。
1091. Shortest Path in Binary Matrix
利用队列,我们可以很直观地利用广度优先搜索,搜索最少扩展层数,即最短到达目的地的距离。注意不要重复搜索相同位置。
934. Shortest Bridge
本题实际上是求两个岛屿间的最短距离,因此我们可以先通过任意搜索方法找到其中一个岛屿,然后利用广度优先搜索,查找其与另一个岛屿的最短距离。
126. Word Ladder II
我们并不是直接从起始节点进行广度优先搜索,直到找到终止节点为止;而是从起始节点和终止节点分别进行广度优先搜索,每次只延展当前层节点数最少的那一端,这样我们可以减少搜索的总结点数。
练习
130. Surrounded Regions
先从最外侧填充,然后再考虑里侧。
257. Binary Tree Paths
输出二叉树中所有从根到叶子的路径,回溯法使用与否有什么区别?
47. Permutations II
排列题的 follow-up,如何处理重复元素?
40. Combination Sum II
组合题的 follow-up,如何处理重复元素?
37. Sudoku Solver
十分经典的数独题,可以利用回溯法求解。事实上对于数独类型的题,有很多进阶的搜索方法和剪枝策略可以提高速度,如启发式搜索。
310. Minimum Height Trees
如何将这道题转为搜索类型题?是使用深度优先还是广度优先呢?