最近在LeetCode集中刷了一部分深度优先搜索的题,在这里做一个总结。对于深度优先搜索只做简单回顾,本文主要是介绍LeetCode中应用深度优先搜索的一类题。
1)深度优先搜索简述
假设使用深度优先搜索遍历一个连通的图G。在遍历之前,为每个结点设置了一个颜色字段color,以标志该结点当前的访问状态。之后我们初始化每个结点的color字段为WHITE,代表该结点还未访问;在遍历过程中,我们每发现一个结点就将该结点的color字段设置为GRAY,以此代表该结点已经被访问。则深度优先搜索可以写成这样:
def DFS(begin_node):
# 将当前顶点标记为GRAY,代表已经访问
begin_node.color = GRAY
# 访问当前顶点的邻接顶点
for adj_node in begin_node.adj_node_list:
if adj_node.color == WHITE:
DFS(adj_node)
2)例题
LeetCode中有这么一类使用深度优先搜索的题,它的特点是:被搜索的“图”是一个矩阵(并不是指图是使用邻接矩阵的形式存储,而是指矩阵中每一个元素对应一个图的结点)。以LeetCode中的第130题为例:
1.原题描述:
给出一个矩阵board,其中元素仅为‘X’或者‘O’,要求设计一个算法将所有被‘X’包围的‘O’替换为‘X’。规定:与矩阵边界相连的‘O’不算被‘X’包围。
示例:
输入:
board =
[[X,X,X,X],
[X,O,O,X],
[X,X,O,X],
[X,O,X,X],
[X,O,X,X]]
输出应将第二行和第三行中被完全包围的'O'替换为'X'(最后两行中的'O'因为与边界相连,所以不应替换):
[[X,X,X,X],
[X,X,X,X],
[X,X,X,X],
[X,O,X,X],
[X,O,X,X]]
2.解题思路:这道题让我们找出被X包围的O,并将其替换为X。这里我们做一个转换,先通过深度优先搜索找出所有与边界相连的O,它们都是不需要被替换的,我们将其先标记为S;之后board中剩余的O便是所有需要用X替换的。在完成替换之后,将标记S重新还原为O。整体上可以写成这样:
def solve(board):
# 从board[row_index][col_index]开始进行深度优先搜索
def DFS(row_index, col_index):
if row_index < 0 or row_index >= len(board) or col_index < 0 or col_index >= len(board[0]):
return
# 'O'代表尚未访问的结点
if board[row_index][col_index] == 'O':
# 将当前结点置为S,代表已经访问
board[row_index][col_index] = 'S'
# 递归搜索当前顶点的邻接顶点
DFS(row_index + 1, col_index)
DFS(row_index - 1, col_index)
DFS(row_index, col_index - 1)
DFS(row_index, col_index + 1)
return
# 对位于边缘同时为‘O’的元素,以其为起点进行深度优先搜索
for i in range(0, len(board)):
for j in range(0, len(board[0])):
if i == 0 or j == 0 or i == len(board) - 1 or j == len(board[0]) - 1:
DFS(i, j)
# 将S改回O,将O转换为X
for i in range(0, len(board)):
for j in range(0, len(board[0])):
if board[i][j] == 'S':
board[i][j] = 'O'
elif board[i][j] == 'O':
board[i][j] = 'X'
return
3.另一种方法
在上面的方法中我们先找出与边界连接的O,并将其标记,从而间接找到需要转换为X的O。考虑一种正面求解的思路:假设board[i][j]为'O',判断它是否需要被反转成X,只需判断,是否存在一条仅由O组成的,从board[i][j]到边界的路径。如果存在这样一条路径,则说明board[i][j]间接的与边界相连,不应转换为X,反之,则应该转换为X。这种思路是没问题的,但是千万不要在递归的过程中对board中的O进行替换修改,就是下面这种写法:
def DFS(row_index, col_index):
# 对DFS终止的处理,和上一种是类似的
…
# 标记位置(row_index, col_index)为已经访问
# 这个标记是必要的,否则就会在相邻的两个结点之间无限递归
board[row_index][col_index] = …
# 遍历邻接顶点,这里简写了
res = [DFS(adj_node) for adj_node in adj_node_list]
# 所有邻接点都返回假,则说明路径不存在,替换‘O’为‘X’
if all([adj_res == False for adj_res in res]):
board[row_index][col_index] = ‘X’
return False
else:
#否则,清除位置(row_index, col_index)的访问标记,并返回,清除的操作这里省略了
return True
这种写法是有问题的,考虑下面的一种情况:
我们从蓝色的O开始进行深度优先搜索。当向它左边邻接顶点进行递归搜索时,最终发现边界,则认为递归过程中经过的O都是不需要转换为X的。但是蓝色O下边的邻接顶点就不是这个样子了。考虑递归搜索到顶点a时,a会搜索到它的邻接顶点均为X,注意,当访问a时,a上边的顶点已经被标记为已访问的状态,从而使a不能对其进行重复的搜索,如下图:
问题就出在这里。这种方法会将顶点a视为需要替换的顶点,之后向“上一层”返回。同理,顶点a上边的顶点也会被认为是需要替换的顶点。所以最终的结果是:
这种方法错误的原因在于将矩阵描述的图当做了一个有向图,而实际上矩阵描述了一个无向图。拿上面的例子来说,这种方法认为蓝色顶点的右邻接点和下邻接点是完全“隔离”的,而事实上它们是间接相连的,这就导致了对邻接点的遍历的结果可能是错误的,当然,递归前的根结点一定是正确的。所以要使用这种方法,我们就不能在递归的过程中更新board,只能以每个O为起点进行深度优先搜索,判断路径是否存在,之后根据最终的返回结果决定是否反转当前的O。可以看出来,对一整块'O'连接的区域遍历,却只可能修改一个位置,并且对同一块区域需要从多个不同的顶点出发遍历,非常耗时。当然,也不能在递归的过程中设置缓存记录中间结果,原因和上面相同:递归的搜索可能是错的。
3)其他相似的题:
200. Number of Islands
417. Pacific Atlantic Water Flow
其中这道417题和这一篇讲的十分类似,可以尝试一下。