vlambda博客
学习文章列表

走出迷宫————深度优先和广度优先搜索

橙泥

HDUGAMEMAKER

程序组 组员


      大家肯定都玩过迷宫,我们需要想方设法帮助玩家(或者是小球什么的)在错综复杂的迷宫之中经过多次的尝试,最终寻找出一条正确的道路走到终点。迷宫在游戏里也经常出现,除了小时候玩的最简单的迷宫游戏以外,一些RPG游戏里的地图就是一个小迷宫,有些游戏也会在其中加入迷宫的因素,比如仙剑奇侠传里的迷宫,宝可梦里的迷宫,真三国无双里的石门八阵,等等等等。


    传说中的第一座迷宫是希腊神话里米诺斯的迷宫,由名匠代达罗斯为克里特岛的国王米诺斯所设计,建造于克诺索斯。这座迷宫用来囚禁米诺斯的儿子,半人半牛怪物的弥诺陶洛斯。代达罗斯巧妙地建造这座迷宫,使得在完成后他本人几乎无法从中逃脱。雅典英雄忒修斯得到阿里阿德涅的帮助,带着毛线的一端进入迷宫,在杀害弥诺陶洛斯后,顺着阿里阿德涅的毛线带领之下成功逃出迷宫。

走出迷宫————深度优先和广度优先搜索

米诺斯牛牛


当我们面对一个迷宫的时候,我们会怎么做——一次次试,也许一次就能找到正确的道路,也许会走进无数次死路,但最终总能走出迷宫(除非他没设计出去的路,那真的没办法)。那么,当拥有计算机的我们面对迷宫问题时,我们是不是能找到什么方便快捷(对于我们来说)的办法来解决呢?忒修斯能够逃离迷宫是因为有阿里阿德涅的帮助,但我们没有迷宫外的阿里阿德涅,只有手里的无限长的毛线,又应该怎么办呢。这里就提出了深度优先搜索这一程序设计方法,利用计算机给予我们的这条近似于无限长的毛线,走出迷宫!

       我们将毛线放在起点,手拿着一端出发,遇到岔路口选择一个方向继续前进,直到走到终点或者死路,如果很不幸走到死路,我们就沿着毛线向回走,沿途收回毛线,直到走到岔路口,换另一个方向前进,如果遇到岔路口或者死路,重复以上行为,最终一定能够走到终点。这个做法的原理,叫做递归-回溯,而这个过程实现,就是我们常说的深度优先搜索(dfs)。当我们走到死路时,我们通过毛线回溯到上一次做出选择的地方(岔路口),更换选择并继续。这样做可以遍历所有的可能情况。下面让我们结合图例和伪代码来更好的理解一下该算法的思想。

走出迷宫————深度优先和广度优先搜索

深度优先搜索


通过上面的图例可以非常直观的了解深度优先搜索的工作方式。下面来分析一下如何用代码来实现它。

       大家都知道,深度优先的主要思想就是“不撞南墙不回头”,“一条路走到黑”,如果遇到“墙”或者“无路可走”时再去走下一条路。

       所以先规定好一个走路的规则,比如就按照右下左上顺时针的方式去尝试。

       如上图僵尸的位置是起始点,先往右走,但是有堵墙走不了,所以按照规定尝试往下走。到达“1”的位置,此时重复刚才的步骤,先向右走可以走到“2”,再重复规则发现向右可以走到“3”,再重复发现“下右左上”四个方向都不能走,这时候就退回“2”的位置尝试向下走……依次类推直到走到最终的目的地。

 

       聪明的你肯定已经发现了“递归”是实现深度优先搜索的最好的方式。定义好规则然后就这样递归的循环下去直到走到终点,请看下面的伪代码:


void dfs(当前位置){//深度优先搜索函数

       为终点或死路

              如果为终点,结束搜索

              如果为死路,回到上一次函数

       为岔路口

              选择第一个路口

                     dfs(下一步的位置)

              选择第二个路口

                     dfs(下一步的位置)

              ...

              选择第n个路口

                     dfs(下一步的位置)     

       为单向通道

              dfs(下一步位置)

}

以上算法是建立在迷宫是一个无环迷宫(即迷宫道路不存在走回自身当前位置的可能)的前提下的,如果迷宫是有环的,我们则可以在走过的每一个位置处留下一个印记表示自己来过这里,如果下一次再走到(而不是退回)这个位置,就说明无法经过这个位置走出迷宫了,可以直接回马另寻他路。

       事实上,我们可以将迷宫简化成一个无向图,将一个个位置设成一个个点,在不同的两个点之间连一条边,这样可以更加清晰直观地观察到每一次搜索。

走出迷宫————深度优先和广度优先搜索

无向图


深度优先搜索是在图论中经常应用的一个非常基础的图形搜索算法,也可以用来解决其他可以简化为图的问题,常见的应用有求解全排列和八皇后问题等,感兴趣的同学可以自行查找资料。它的过程其实是一种优化过的枚举,力求于枚举所有可能以判断问题是否有解或求其他内容,相对来说时间复杂度还是偏高,接下来介绍它的姊妹算法——广度优先搜索(bfs)

      相比于深度优先搜索,广度优先搜索的效果更偏向于寻找最短的的路径,因此应用于多条线路的迷宫中寻找最快逃离路径更好一些。

      广度优先搜索比较好理解一些,将迷宫简化为无向图,我们从起点开始,枚举所有可能的下一步,如果这个点从未进过队列,则记算其距离并将其存储到一个队列的末端(队列可以理解为一个先进先出的数列),然后优先对于离起点更近的点重复这个过程,可以发现,后进入队列的点比先进入队列的点离起点的距离一定更远或相等,通过这个过程我们就可以得到每个点离起点的最短距离了,当我们枚举到出口时,自然也得到了从出口到起点的最短距离。同样让我们结合图片和伪代码来理解一下。

走出迷宫————深度优先和广度优先搜索

广度优先搜索


通过两个图例的对比,可以清楚的看到两种搜索算法的区别。

       深度优先就是一条路走到底,然后再尝试下一条路。

       广度优先就是走到一个点,将该点周围可走的点走完,然后再按同样规则走其他的点。(大家可以想像一下铺满的多米诺骨牌,将其中一块推倒其余周围的骨牌也会一层层扩散式的倒塌)

       所以先规定好一个走路的规则,同样按照右下左上顺时针的方式去尝试。

       如上图僵尸的位置是起始点,先往右走,但是有堵墙走不了,所以按照规定尝试往下走。到达“1”的位置,在“1”处可以知道四周能走的点只有“2”。“2”能走的点有“3”,“4”。来到“3”,发现没有可走的。来到“4”发现去可以去“5”。这样依次进行下去就完成了广度优先搜索。

        我们可以通过“队列”来模拟上面的过程,伪代码如下:


void bfs(){//广度优先搜索

       将起点放入队列

       while(队列不为空) {

              取出队首点

              枚举其能到达的所有未进过队的点

                     计算距离并放入队列

                     标记这个点进过队列

       }

}


正如前文所述,这样并不一定能很快的找出出口(当然深度优先搜索也不一定,要看运气),但是可以得到每个点到起点的距离,广度优先搜索的应用更趋向于求解最短路之类的问题和其他需要搜索最优解的问题,其他更先进的最短路算法也是基于bfs的基础上实现的,可谓也是非常重要的一个算法。

       用一个不太恰当的比喻,深度优先搜索像是艾克的时空断裂,走到死路可以“再来一次”,回到上次做选择的地方换个方法;广度优先搜索像是鸣人的影分身,在每次遇到岔路口的时候分出分身,每个分身独立继续前进。两种算法各有利弊,可以视情况而选择。

       在这里顺便插入一个MC中走迷宫的好办法:右手火把定则。在走迷宫的时候永远把火把插在右手边的墙壁上,这样即使出现环路导致方向混淆,也可以通过左手的火把回到来时的地方,可谓是一个非常实用的小技巧。(没什么用的知识增加了)

走出迷宫————深度优先和广度优先搜索

没什么用的知识


现在,再看到密密麻麻的迷宫,是不是觉得其简单了一些呢。


原文:https://blog.csdn.net/a396901990/article/details/45028741


HduGameMaker

文案|橙泥

编辑|Krasus