動(dòng)畫演示深度優(yōu)先算法搜尋逃出迷宮的路徑

深度優(yōu)先算法(DFS 算法)是什么塔鳍?

尋找起始節(jié)點(diǎn)與目標(biāo)節(jié)點(diǎn)之間路徑的算法,常用于搜索逃出迷宮的路徑拜隧。主要思想是宿百,從入口開始,依次搜尋周圍可能的節(jié)點(diǎn)坐標(biāo)洪添,但不會(huì)重復(fù)經(jīng)過同一個(gè)節(jié)點(diǎn)垦页,且不能通過障礙節(jié)點(diǎn)。如果走到某個(gè)節(jié)點(diǎn)發(fā)現(xiàn)無路可走干奢,那么就會(huì)回退到上一個(gè)節(jié)點(diǎn)痊焊,重新選擇其他路徑。直到找到出口忿峻,或者退到起點(diǎn)再也無路可走薄啥,游戲結(jié)束。當(dāng)然逛尚,深度優(yōu)先算法垄惧,只要查找到一條行得通的路徑,就會(huì)停止搜索绰寞;也就是說只要有路可走到逊,深度優(yōu)先算法就不會(huì)回退到上一步。

下圖是使用 DFS 算法搜尋出來的一條路徑:

在這里插入圖片描述

總結(jié)一下:

從起點(diǎn)開始滤钱,查詢下一步走得通的節(jié)點(diǎn)觉壶,將這些可能的節(jié)點(diǎn)壓入堆棧中,已經(jīng)走過的節(jié)點(diǎn)不再嘗試件缸。查詢完畢之后掰曾,從堆棧中取出一個(gè)節(jié)點(diǎn),查詢該節(jié)點(diǎn)周圍是否存在走得通的節(jié)點(diǎn)停团。如果不存在可能的節(jié)點(diǎn),就繼續(xù)從堆棧中取一個(gè)節(jié)點(diǎn)掏熬。重復(fù)以上操作佑稠,直到當(dāng)前節(jié)點(diǎn)為終點(diǎn),或者堆棧中再無節(jié)點(diǎn)旗芬。

定義數(shù)據(jù):

  • 起始節(jié)點(diǎn)與目標(biāo)節(jié)點(diǎn)
  • 存儲(chǔ)節(jié)點(diǎn)的堆棧

定義輔助函數(shù)

  • 獲取下一節(jié)點(diǎn)的函數(shù): successor
  • 判斷是否為終點(diǎn)的函數(shù): test_goal

首先舌胶,我們來定義棧這種數(shù)據(jù)結(jié)構(gòu),棧是一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)疮丛,之前公眾號寫過一篇介紹性文章:

https://mp.weixin.qq.com/s/adC4Y3YWCuvLmjSuj90Mrw

因?yàn)橹蟮膹V度優(yōu)先搜索會(huì)使用到隊(duì)列幔嫂,A* 算法會(huì)用到優(yōu)先隊(duì)列辆它,我們定義了抽象基類,以便后續(xù)使用履恩。deque 是雙端隊(duì)列锰茉,與內(nèi)置類型 list 操作類似,但頭部與尾部插入和刪除操作的時(shí)間復(fù)雜度均為 O(1)切心。

# utils.py
from abc import abstractmethod, ABC
from collections import deque

class Base(ABC):
    def __init__(self):
        self._container = deque()

    @abstractmethod
    def push(self, value):
        """push item"""

    @abstractmethod
    def pop(self):
        """pop item"""

    def __len__(self):
        return len(self._container)

    def __repr__(self):
        return f'{type(self).__name__}({list(self._container)})'


class Stack(Base):
    def push(self, value):
        self._container.append(value)

    def pop(self):
        return self._container.pop()

下面我們來定義 dfs 函數(shù)飒筑。其中,initial 為初始節(jié)點(diǎn), s 為棧绽昏,marked 用來記錄經(jīng)過的節(jié)點(diǎn)协屡。successor 函數(shù)用來搜尋下一個(gè)可能的節(jié)點(diǎn),test_goal 函數(shù)用來判斷該節(jié)點(diǎn)是否為目標(biāo)節(jié)點(diǎn)全谤。children 為可能的節(jié)點(diǎn)列表肤晓,遍歷這些節(jié)點(diǎn),將沒有走過的節(jié)點(diǎn)壓入棧中认然,并做記錄补憾。

# find_path.py
from utils import Stack

def dfs(initial, _next = successor, _test = test_goal):
    s: Stack = Stack()
    marked = {initial}
    s.push(initial)
    while s:
        parent = s.pop()
        if _test(parent):
            return parent
        children = _next(parent)
        for child in children:
            if child not in marked:
                marked.add(child)
                s.push(child)

接下來季眷,我們使用 DFS 算法尋找迷宮路徑余蟹,并對搜尋到的迷宮路徑進(jìn)行可視化演示。

首先使用枚舉子刮,來表示路徑的顏色, EMPTY 為正常節(jié)點(diǎn)威酒,BLOCKED 為障礙節(jié)點(diǎn),START 為迷宮入口挺峡,END 為迷宮出口葵孤,PATH 為搜尋的路徑。

from enum import IntEnum

class Cell(IntEnum):
    EMPTY = 255
    BLOCKED = 0
    START = 100
    END = 200
    PATH = 150

接下來橱赠,我們來定義迷宮尤仍。首先,我們采用 Namedtuple 來定義迷宮每個(gè)節(jié)點(diǎn)的坐標(biāo):

class MazeLocation(NamedTuple):
    row: int
    col: int

首先為了方便確定節(jié)點(diǎn)之間的關(guān)系狭姨,我們在 Maze 類中定義了一個(gè)內(nèi)部類 _Node, 用來記錄節(jié)點(diǎn)的狀態(tài)宰啦,及節(jié)點(diǎn)的父節(jié)點(diǎn)。

class _Node:
    def __init__(self, state, parent):
        self.state = state
        self.parent = parent

接著初始化饼拍,確定入口與出口的坐標(biāo)赡模,使用 np.random.choice 函數(shù)隨機(jī)生成迷宮,并標(biāo)記入口和出口师抄。

def __init__(self, rows: int = 10, cols: int = 10,
             sparse: float = 0.2, seed: int = 365,
             start: MazeLocation = MazeLocation(0, 0),
             end: MazeLocation = MazeLocation(9, 9), *,
             grid: Optional[np.array] = None) -> None:
    np.random.seed(seed)
    self._start: MazeLocation = start
    self._end: MazeLocation = end
    self._grid: np.array = np.random.choice([Cell.BLOCKED, Cell.EMPTY],
                                                (rows, cols), p=[sparse, 1 - sparse])
    self._grid[start] = Cell.START
    self._grid[end] = Cell.END

其次是 test_goal 方法漓柑,只要該節(jié)點(diǎn)坐標(biāo)與目標(biāo)節(jié)點(diǎn)相即可。

def _test_goal(self, m1: MazeLocation) -> bool:
    return m1 == self._end

再就是 successor 方法,只要上下左右方向的節(jié)點(diǎn)不是障礙節(jié)點(diǎn)且在邊界之內(nèi)辆布,就納入考慮范圍瞬矩,加入列表之中。

def _success(self, m1: MazeLocation) -> List[MazeLocation]:
    location: List[MazeLocation] = []
    row, col = self._grid.shape
    if m1.row + 1 < row and self._grid[m1.row + 1, m1.col] != Cell.BLOCKED:
        location.append(MazeLocation(m1.row + 1, m1.col))
    if m1.row - 1 >= 0 and self._grid[m1.row - 1, m1.col] != Cell.BLOCKED:
        location.append(MazeLocation(m1.row - 1, m1.col))
    if m1.col + 1 < col and self._grid[m1.row, m1.col + 1] != Cell.BLOCKED:
        location.append(MazeLocation(m1.row, m1.col + 1))
    if m1.col - 1 >= 0 and self._grid[m1.row, m1.col - 1] != Cell.BLOCKED:
        location.append(MazeLocation(m1.row, m1.col - 1))
    return location

顯示路徑, pause 為顯示圖像的間隔,plot 為是否繪圖標(biāo)志锋玲。通過目標(biāo)節(jié)點(diǎn)出發(fā)景用,遍歷每一個(gè)節(jié)點(diǎn)的父節(jié)點(diǎn),直到到達(dá)初始節(jié)點(diǎn)嫩絮,并繪制路徑圖丛肢。

def show_path(self, pause: float = 0.5, *, plot: bool = True) -> None:
    if pause <= 0:
        raise ValueError('pause must be more than 0')
    path: Maze._Node = self._search()
    if path is None:
        print('沒有找到路徑')
        return
    path = path.parent
    while path.parent is not None:
        self._grid[path.state] = Cell.PATH
        if plot:
            self._draw(pause)
        path = path.parent

    print('Path Done')

為了使用 DFS 算法,我們定義了 DepthFirstSearch 類剿干,繼承迷宮類蜂怎。DepthFirstSearch 類重寫了基類的 _search 方法,與我們之前定義的 dfs 函數(shù)定義相差無幾置尔。

class DepthFirstSearch(Maze):
    def _search(self):
        stack: Stack = Stack()
        initial: DepthFirstSearch._Node = self._Node(self._start, None)
        marked: Set[MazeLocation] = {initial.state}
        stack.push(initial)
        while stack:
            parent: DepthFirstSearch._Node = stack.pop()
            state: MazeLocation = parent.state
            if self._test_goal(state):
                return parent
            children: List[MazeLocation] = self._success(state)
            for child in children:
                if child not in marked:
                    marked.add(child)
                    stack.push(self._Node(child, parent))

最后再放一張效果圖:

在這里插入圖片描述
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杠步,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子榜轿,更是在濱河造成了極大的恐慌幽歼,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谬盐,死亡現(xiàn)場離奇詭異甸私,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)飞傀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門皇型,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人砸烦,你說我怎么就攤上這事弃鸦。” “怎么了幢痘?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵唬格,是天一觀的道長。 經(jīng)常有香客問我颜说,道長购岗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任门粪,我火速辦了婚禮藕畔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庄拇。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布措近。 她就那樣靜靜地躺著溶弟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瞭郑。 梳的紋絲不亂的頭發(fā)上辜御,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音屈张,去河邊找鬼擒权。 笑死,一個(gè)胖子當(dāng)著我的面吹牛阁谆,可吹牛的內(nèi)容都是我干的碳抄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼场绿,長吁一口氣:“原來是場噩夢啊……” “哼剖效!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起焰盗,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤璧尸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后熬拒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爷光,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年澎粟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蛀序。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捌议,死狀恐怖哼拔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瓣颅,我是刑警寧澤倦逐,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站宫补,受9級特大地震影響檬姥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜粉怕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一健民、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贫贝,春花似錦秉犹、人聲如沸蛉谜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽型诚。三九已至,卻和暖如春鸳劳,著一層夾襖步出監(jiān)牢的瞬間狰贯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工赏廓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涵紊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓幔摸,卻偏偏與公主長得像摸柄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子抚太,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容