算法小课堂:走迷宫之DFS vs BFS

前言

  上期我们说到DFS的经典入门题目——Fibonaci数列的第n项,这次我们来说下DFS或者BFS较为经典的走迷宫题目解法,很多关于迷宫类题目的母题都是这个了,而且也很容易理解,冲冲冲!

  我们约定迷宫里出口用S表示,出口用T表示,可以通行(不是一方通行)的格子用点表示,障碍物(即不能通行)的格子用*表示。

  类似如下的输入形式:

....s*
.*****
....*.
***..*
.T....

  今天我们利用两种思路,分别是DFS和BFS来解决这个问题。

CSDN (゜-゜)つロ 干杯

DFS走迷宫

算法思路

  DFS方法来解决的思路类似于我们上次说的老鼠走迷宫,真就无限套娃呗……从一条路一直走到底A,如果碰到障碍物,就退回一步到B,在B处尝试完所有的可能方向,如果还不行,继续回退到C,循环往复,直到找到可行的通路。如果没有,则表示没有通路。

  这里有几个点需要注意:

  1. 怎么表示走的过程,实际上就是你在二维数组操作的过程
  2. 走的过程中坐标移动时不应该超过地图范围
  3. 迷宫可能有多条通路,应该选择最近的一条
  4. 回退过程中怎么标记已经查看过的节点避免循环bug
  5. 最后一个是我们整体的逻辑思路:即上面一段话的程序实现

  因为是开始讲解DFS的算法题解,我会尽量详细,让大家没有太多理解负担,本身也是为了通俗易懂的讲解算法知识。


代码实现细节

  首先我们来读取迷宫数据并标记入口和出口,以及各种障碍物

# 读取maze数据的行列数
row,col = list(map(int,input().split()))
# 存储maze数据的二维数组
maze_data = []
for i in range(row):
    # maze_data.append([s for s in input()])
    In = input()
    maze_data.append([s for s in In])
#starting point
s = (0, 0)
for r in maze_data:
    if 's' in r:
        # 获取起点的行列坐标,从0开始计数
        s = (maze_data.index(r), r.index('s'))

  接下来我们定义走的方向,由于我们只是上下左右移动(四连通),其实就是行列坐标的加一减一:

扫描二维码关注公众号,回复: 12259170 查看本文章
# 行走方向,四连通方式
direction = [[-1, 0], [0, -1], [1, 0], [0, 1]]

  还有走的时候不应该超过maze的范围,即超过范围的行列坐标pass

# check valid coordinates
def check(x,y):
		nonlocal row,col
		return 0 <= x < row and 0 <= y < col

  这里nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,且只能用于嵌套函数中,因为我们是在一个函数里定义的check函数,所以适用。一般这么用主要是为了避免使用global这种比较不安全和暴力的全局变量声明方式。

  接下来我们来看看一些其他变量的定义:

# 初始化最远路径长度
distance = 100000
# 初始化标记数组,用来避免重复进入节点
max_r_c = max(row, col)
vis = [[0 for _ in range(max_r_c)] for _ in range(max_r_c)]
# 可行通路数量counter
num_paths = 0

  vis数组这里的作用类似于给一个节点能不能走打标签,当我们进入节点A,之后进入节点B,那么我们要将vis里对应A的位置标记,否则再遍历B的可行方向时又会回到A,A又会进入B,导致循环bug

  而当我们退出一个节点C的所有子状态时,应该取消vis里对应的C的标记,因为这仅仅表示这条通路经过C无法通走,不表示其他节点不能通过C组成通路。

  有了上面的准备,我们可以来进行DFS核心函数的编写:

def dfs(x, y, steps):
    """
    maze core function
    Args:
    x (int): row index start from 0
    y (int): col index start from 0
    steps (int): current steps from starting point
    """
    nonlocal vis, distance, maze_data, num_paths
    # if reach the end point
    if maze_data[x][y] == 'T':
        # get shortest one
        distance = min(distance, steps)
        # counter fresh
        num_paths += 1
        # mark when enter node
    vis[x][y] = 1
    # find possible direction
    for i in range(4):
        tx = x + direction[i][0]
        ty = y + direction[i][1]
        if check(tx, ty) and maze_data[tx][ty] != '*' and not vis[tx][ty]:
            dfs(tx, ty, steps + 1)
    # remove marker when exit node
    vis[x][y] = 0

  其中下面这个判断就是我们的核心思路:

if check(tx, ty) and maze_data[tx][ty] != '*' and not vis[tx][ty]:
    dfs(tx, ty, steps+1)

  当下一步的格子在maze范围内,而且不是障碍物*,并且还没有被标记过表示可以通行,则递归到该点。

  再来点收尾工作:

# dfs from starting point with step 0
dfs(s[0], s[1], 0)
print("Get {}  practical paths".format(num_paths))
print("Shortest distance:{}".format(distance))

样例测试

  我们用刚才的例子来进行测试:

>>> dfs_maze()
5 6
....s*
.***.*
......
***..*
.T.*.*
No Solution
Using time: 0.00400s

>>> dfs_maze()
5 6
....s*
.*****
....*.
***..*
.T....
Get 2  practical paths
Shortest distance:13
Using time: 0.00700s

  如果你在dfs的过程中加入路径记录的操作就可以显示最短的路径啦,这里我直接给出最短路径的标识(用m标记):

mmmmS*
m*****
mmmm*.
***m.*
.Tmm..

在这里插入图片描述


BFS走迷宫

算法原理

  DFS可以寻找最短路径,其实BFS也可以,它们两者最大的区别在于搜索方式的不同。BFS即广度优先搜索,以走迷宫为例形象的说就是当你在一个节点时,不是一条路走到底,而是先上下左右的格子都走一遍,在其中把可行的子节点加到队列里,慢慢地扩大你的搜索半径。就好像把水倒在棋盘格慢慢地从中心格点浸没到周围的点。

  注意BFS常用的数据结构就是队列,先进先出FIFO。(关于队列的知识小伙伴自行百度学习喔~)

在这里插入图片描述

  BFS自带最短特性,在起始点已知的情况下,每一步都是以起始点为中心向外半径不断增大辐射的,所以一旦找到可行的通路,就是最短的路径,是不是很nice!

在这里插入图片描述


代码实现细节

  maze的读入和一些常用变量和方向的定义跟DFS其实是一样,这里我们在BFS中加入保存最短路径的操作。

from queue import Queue

# 保存最短路径上每个节点的上一个节点位置
Parent  =[[0 for _ in range(max_r_c)] for _ in range(max_r_c)]

# 打印最短路径
def print_result_path(x, y, flag = 'm'):
    """
	print maze data with shortest pathway
	Args:
		x (int): row index start from 0
		y (int): col index start from 0
		flag (str, optional): shortest pathway marker. Defaults to 'm'.
	"""
	nonlocal Parent,maze_data,row
    
	while Parent[x][y]:
		maze_data[x][y] = flag
        # fresh coordinates
		x, y = Parent[x][y]
        
	# show maze
	for i in range(row):
		print(''.join(str(s) for s in maze_data[i]))

  其中,Parent是保存最短路径上每一个节点父节点位置的一个数组,通过它我们可以标记出整条道路。

  我们来看看BFS的核心逻辑:

# initial Queue
Q = Queue()
# add starting point
Q.put((s[0], s[1], 0))
# current step initial
step = 0
# when queue is not empty
while Q.qsize() != 0:
    # get next node
    x, y, step = Q.get()
    # end point check
    if maze_data[x][y] == 'T':
        print("Shortest distance:{}".format(step))
        print_result_path(x, y)
        return
    # mark node
    vis[x][y] = 1
    for d in direction:
        nx, ny = x+d[0], y+d[1]
        if check(nx, ny) and maze_data[nx][ny] != '*' and not vis[nx][ny]:
            # add next possible node in queue
            Q.put((nx, ny, step+1))
    		# record parent node coordinates
            Parent[nx][ny] = [x, y]

  其中,Python里队Queue的用法请自行百度,code里也只是展示了最基本的用法~

  由于BFS的自带最短特性,所以我们只需要进入节点时标记,而不需要去掉标记的步骤因为不存在回溯的过程,一旦找到出口那就是最短路径啦~


样例测试

  我们来看看测试结果:

>>> bfs_maze()
5 6
....s*
.***.*
......
***..*
.T.*.*
No Solution
Using time: 0.00300s
>>> bfs_maze()
5 6
....s*
.*****
....*.
***..*
.T....
Shortest distance:13
mmmms*
m*****
mmmm*.
***m.*
.mmm..
Using time: 0.02200s

两种方法的对比

  这里BFS的运行时间是0.02s左右,而DFS是0.007s左右,小伙子你有问题 不讲code德。 原理上BFS是最短特性搜索,应该比DFS全局搜索更快。

在这里插入图片描述

  其实BFS相比DFS在结构上由于使用了队列,在开辟内存块和存取上操作更多,问题规模数不大的情况下自然也就相对来说耗费了更多的时间。简单问题上不能显示出BFS的优势,我们这里来测试一个复杂一些的maze。

  如以下的一个maze:

**********************
*...*...**.**.....*..T
*.*...*.......***.*.**
*.*.*..**..****...*.**
***.******....***.*..*
*..........**..**....*
*****.******...*****.*
*.....*...*******..*.*
*.*******......S.*...*
*................*.***
******************.***

  来看看各自的测试结果(省略输入的打印):

>>> dfs_maze()
……
Get 768  practical paths
Shortest distance:51
Using time: 0.10801s

>>> bfs_maze()
……
Shortest distance:51
**********************
*...*...**.**mmmmm*mmT
*.*...*...mmmm***m*m**
*.*.*..**.m****..m*m**
***.******m...***m*m.*
*....mmmmmm**..**mmm.*
*****m******...*****.*
*mmmmm*...*******..*.*
*m*******......S.*...*
*mmmmmmmmmmmmmmm.*.***
******************.***
Using time: 0.04600s

  可以看到BFS的优势已经显示出来,DFS找到了所有的可能通路,一共768条,然后选出最少的一条。在只需要得到最短路径及其长度的情况下,显然BFS的方法更优


总结

  相信通过本次的DFS和BFS迷宫经典问题讲解,大家可以对利用DFS来解决问题有一个更好的理解,关于DFS和BFS的相爱相杀我们后续还会继续嗑瓜~


WeChat

CSDN BLog

快来跟小刀一起头秃~

Reference

  [1] 算法小课堂:走迷宫之DFS vs BFS

猜你喜欢

转载自blog.csdn.net/weixin_40519529/article/details/113081269
今日推荐