深度优先搜索_LeetCode中的深度优先搜索(一)

最近在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

这种写法是有问题的,考虑下面的一种情况:

2a15993955dbaabd217097f7ccf15534.png

我们从蓝色的O开始进行深度优先搜索。当向它左边邻接顶点进行递归搜索时,最终发现边界,则认为递归过程中经过的O都是不需要转换为X的。但是蓝色O下边的邻接顶点就不是这个样子了。考虑递归搜索到顶点a时,a会搜索到它的邻接顶点均为X,注意,当访问a时,a上边的顶点已经被标记为已访问的状态,从而使a不能对其进行重复的搜索,如下图:

3a611b4c4fc6608ddf7b33e132175e10.png

问题就出在这里。这种方法会将顶点a视为需要替换的顶点,之后向“上一层”返回。同理,顶点a上边的顶点也会被认为是需要替换的顶点。所以最终的结果是:

f1b827834165750390f52620209367c5.png

这种方法错误的原因在于将矩阵描述的图当做了一个有向图,而实际上矩阵描述了一个无向图。拿上面的例子来说,这种方法认为蓝色顶点的右邻接点和下邻接点是完全“隔离”的,而事实上它们是间接相连的,这就导致了对邻接点的遍历的结果可能是错误的,当然,递归前的根结点一定是正确的。所以要使用这种方法,我们就不能在递归的过程中更新board,只能以每个O为起点进行深度优先搜索,判断路径是否存在,之后根据最终的返回结果决定是否反转当前的O。可以看出来,对一整块'O'连接的区域遍历,却只可能修改一个位置,并且对同一块区域需要从多个不同的顶点出发遍历,非常耗时。当然,也不能在递归的过程中设置缓存记录中间结果,原因和上面相同:递归的搜索可能是错的。

3)其他相似的题:

200. Number of Islands

417. Pacific Atlantic Water Flow

其中这道417题和这一篇讲的十分类似,可以尝试一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值