1. 程式人生 > 其它 >演算法小課堂:走迷宮之DFS vs BFS

演算法小課堂:走迷宮之DFS vs BFS

技術標籤:演算法小課堂演算法pythondfsbfsalgorithm

演算法小課堂:走迷宮之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'))

  接下來我們定義走的方向,由於我們只是上下左右移動(四連通),其實就是行列座標的加一減一:

# 行走方向,四連通方式
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