Algorithm small class: DFS vs BFS in the maze

Preface

  In the last issue, we talked about the classic entry problem of DFS-the nth term of the Fibonaci sequence. This time we will talk about the more classic solution to the maze problem of DFS or BFS. Many motifs about maze problems are this, and also It's easy to understand, go for it!

  We agree that the exit in the maze is denoted by S, the exit is denoted by T, the grids that can pass (not one side) are denoted by dots, and the grids of obstacles (that is, impassable) are denoted by *.

  Input form similar to the following:

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

  Today we use two ideas, DFS and BFS to solve this problem.

CSDN (゜-゜) Cheers つロ

DFS walks the maze

Algorithm ideas

  The idea of ​​DFS method to solve is similar to the mouse we said last time to walk the maze, it is really an infinite nest of dolls... From one road to the end A, if you encounter an obstacle, take a step back to B, and finish at B. All possible directions, if it still does not work, continue to back to C, and iteratively, until a feasible path is found. If not, it means there is no access.

  There are a few points to note here:

  1. How to express the process of walking is actually the process of operating on a two-dimensional array
  2. When moving, the coordinates should not exceed the map range
  3. The maze may have multiple paths, the nearest one should be selected
  4. How to mark the nodes that have been viewed during the rollback process to avoid loop bugs
  5. The last one is our overall logical thinking: the program implementation of the above paragraph

  Because I started to explain the algorithm problem of DFS, I will try to be as detailed as possible, so that everyone does not have too much understanding burden, and it is also for the purpose of explaining the algorithm knowledge in an easy to understand way.


Code implementation details

  First, let's read the maze data and mark the entrance and exit, as well as various obstacles

# 读取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'))

  Next, we define the direction to go. Since we only move up, down, left and right (four connected), it is actually the row and column coordinates plus one minus one:

# 行走方向,四连通方式
direction = [[-1, 0], [0, -1], [1, 0], [0, 1]]

  Also, when walking, it should not exceed the range of maze, that is, the row and column coordinates that exceed the range pass

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

  Here, the nonlocal keyword modifies the variable to indicate that the variable is a local variable in the upper-level function, and can only be used in nested functions , because we are a check function defined in a function, so it is applicable. Generally, it is mainly used to avoid the use of global, which is relatively unsafe and violent global variable declaration method.

  Next we take a look at the definition of some other variables:

# 初始化最远路径长度
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

  The function of the vis array here is similar to labeling whether a node can go . When we enter node A and then enter node B, then we need to mark the position of A in vis, otherwise we will return when we traverse the feasible direction of B. To A, A will enter B again, causing a loop bug .

  When we exit all sub-states of a node C , we should cancel the corresponding C mark in vis, because this only means that this path cannot be passed through C, and it does not mean that other nodes cannot form a path through C.

  With the above preparation, we can write DFS core functions:

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

  The following judgment is our core idea:

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

  When the next grid is within the range of the maze and is not an obstacle*, and has not been marked to indicate that it is passable, then recursively to that point.

  Let's do some finishing work:

# 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))

Sample test

  We use the example just now to test:

>>> 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

  If you add the path record operation in the dfs process, the shortest path can be displayed. Here I directly give the shortest path identifier (marked with m):

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

Insert picture description here


BFS walk the maze

Algorithm principle

  DFS can find the shortest path, in fact, BFS can also. The biggest difference between the two lies in the different search methods. BFS is breadth-first search . Taking walking the maze as an example, it means that when you are at a node, you do not go to the end in one way, but first go through the up and down, left and right grids, and add feasible child nodes to the queue. , Slowly expand your search radius . It's like pouring water on the checkerboard and slowly immersing it from the center point to the surrounding points.

  Note that the commonly used data structure of BFS is queue, first in first out FIFO. (Knowledge about the queue, friends learn from Baidu by themselves~)

Insert picture description here

  BFS comes with the shortest characteristic. When the starting point is known, each step is radiated from the starting point as the center and the outward radius is increasing. So once a feasible path is found, it is the shortest path. Isn't it nice?

Insert picture description here


Code implementation details

  The reading of maze and the definition of some commonly used variables and directions are actually the same as DFS. Here we add the operation of saving the shortest path in 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]))

  Among them, Parent is an array that saves the position of the parent node of each node on the shortest path, through which we can mark the entire road.

  Let's take a look at the core logic of 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]

  Among them, please use Baidu for the usage of Queue in Python , and the code only shows the most basic usage~

  Due to the shortest feature of BFS , we only need to mark when entering the node, without removing the step of marking , because there is no backtracking process, once the exit is found, it is the shortest path~


Sample test

  Let's take a look at the test results:

>>> 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

Comparison of the two methods

  Here, the running time of BFS is about 0.02s, and DFS is about 0.007s. Boy, if you have a problem, don’t talk about code. In principle, BFS is the shortest feature search, which should be faster than DFS global search.

Insert picture description here

  In fact, compared to DFS, BFS uses queues in structure, and has more operations on memory block development and access. Naturally, it takes more time when the problem scale is not large. The advantages of BFS cannot be shown on simple problems. Let's test a more complicated maze.

  Such as the following maze:

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

  Take a look at the respective test results (omit the input printing):

>>> 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

  It can be seen that the advantages of BFS have been shown. DFS has found all possible paths, a total of 768, and then selected the least one. In the case that only the shortest path and its length need to be obtained, the BFS method is obviously better


to sum up

  I believe that through this explanation of the classic problems of DFS and BFS labyrinth, everyone can have a better understanding of using DFS to solve the problem. We will continue to eat about the love and killing of DFS and BFS~


WeChat

CSDN BLog

Come and be bald with the knife~

Reference

  [1] Algorithm small class: DFS vs BFS in the maze

Guess you like

Origin blog.csdn.net/weixin_40519529/article/details/113081269