解迷宫演算法

解迷宫演算法又称走迷宫演算法是一种自动求解迷宫的方法。解迷宫演算法主要可以分成两大类，一种是用来走没走过的迷宫且无法得知整个迷宫的方法，这类方法较常见的有随机老鼠演算法、沿墙法、普莱吉演算法和特雷莫演算法；另一类是适用于可以一次看到整个迷宫时所使用的方法，这类方法较常见的有死路填充法和最短路径演算法。
不包含循环路径的迷宫称为「简单连接」或「完美」的迷宫，其等价于图论中的树。解迷宫演算法与图论密切相关。直观上来说，若以适当的方式拉开迷宫中的路径，其结果可能会是一棵树。
概述.
一个典型的解迷宫演算法会
演算法的输入根据其类型而定：有的演算法是假设了「能以鸟瞰式的方式看到整个迷宫」为前提，所以输入时会描述整个迷宫的环境；有的演算法是设计给机械人实际在迷宫里走迷宫的，所以输入时仅会描述机械人视线范围内的环境。
解迷宫演算法在人工智慧、机器人学甚至是游戏编程（游戏内NPC的寻路演算法）等领域中都有相当的应用。
另一方面，解迷宫演算法也引起了数学家的注意，因为这演算法与图论中的演算法有相当的关连。例如：单连通的迷宫，迷宫中的路线简单地连接著，并且不包含任何循环。这种迷宫相当于图论中的树状图：如果将迷宫中的路线拉开的话会得到类似图论中讨论的树状结构。
随机老鼠演算法.
这是一种非常简单的演算法，可以由非常不智能的机器人或一只普通的老鼠来实现。这个演算法的内容就是单纯的延著迷宫中的路径前进，遇到岔路时，随机选择一个方向来前进。虽然这个方法最终总是能解开迷宫，但过程可能十分缓慢。
沿墙法.
解迷宫演算法中最著名的方法是沿墙法，也称为左手规则（左手法则、左手定则）或右手规则（右手法则、右手定则）。如果迷宫是单连通的，也就是说迷宫的所有墙壁都与迷宫的外边界相连，那么只要靠著迷宫的一侧墙不断前进（或者说每次遇到岔路都转向同个方向，如每次遇岔路都右转或都左转），那么就能保证不迷路地到达迷宫的出口（如果有出口的话），如果迷宫没有出口则会回到原地，且至少遍历了与该墙连接的部分旁边的每个走廊。这个演算法是一个深度优先的中序树遍历。
关于为何沿墙法能解迷宫的另一个观点是基于拓朴学的解释。如果迷宫是单连通的，代表迷宫的墙壁是相连的，且没有回路，所以在一个这样的迷宫里，每当走迷宫那个人遇到岔路，每条岔路只有可能再分岔或者是死路。这就表示如果将这迷宫化成树状图，这个树状图不会有任何互相相交的分支，且在拓朴学上它们可能可以拓朴形变为一个环，然后沿墙法可简化为在这个环上从头到尾绕一圈。为了进一步推进这个想法，可留意将迷宫墙壁的连通-{zh-hans:分量;zh-hant:元件;}-组合在一起，而这些-{zh-hans:分量;zh-hant:元件;}-的边界正是迷宫的解。
如果迷宫不是单连通的，（例如起点和终点位于被环状通道包围之结构的中心、或者路径互相交叉且能解迷宫的路径的部分被环状通道包围）则沿墙法不一定有效。
另一个须留意的点是如果不是在迷宫入口处就开始就依循沿墙法走迷宫时。如果迷宫不是单连通的，并且是在迷宫中的任意点才开始依循沿墙法走迷宫，那么可能会发现自己被困在单独一堵墙之中，该墙壁围绕著自己，并且无法返回入口也无法抵达出口，还会永远沿著这一系列的墙打转。如果是走迷宫半路上才开始依循沿墙法走迷宫时，应尝试记住或标记迷宫中的一个点，并从那个点开始依循沿墙法走迷宫，因为沿墙法总是会带你回到同一个地方，如果第二次又绕回同一个点，那么就能断定迷宫不是单连通的，这时就应该切换到另一面尚未依循的墙或方向。有关的替代方法可以参考下面列出的普莱吉演算法。
普莱吉演算法.
不相交（存在不与外边界相连的墙之迷宫、或边界不封闭的迷宫）的迷宫可以使用沿墙法解决。然而，如果从迷宫内部某点才开始求解迷宫，那么使用沿墙法可能会沿著一道不与出口相连的墙，并且不断绕著这些墙打转。普莱吉演算法（Pledge algorithm；名称取自约翰·普莱吉（John Pledge））可以解决这一问题。
普莱吉演算法是为了要能够避开障碍物而设计的，需要走迷宫那个人也许随便选择一个方向来做前进方向。当依循这个演算法走迷宫那个人撞到一块障碍物的时候，他会转方向，同时会一只手会触摸著旧东西一边转，并且会数旋转的角度（例如顺时针当正，逆时针当负）， 并且不断尝试把总共转了的角变为0度（例如：如果走迷宫者左转了，并走了一格就撞到可以右转的地方的话，他会马上右转）。当走迷宫那个人转到面向回他原本的前进方向时，总共转的角（图里的「S」）会是0度，而走迷宫那个人会离开旧障碍物，继续向著他原本的方向走。
只有当「总共转的角」（图里的「S」）和「当前方向」（图里的「H」）都为零时，手才会从墙上移开。这个演算法能够帮走迷宫的那个人克服像拉丁字母「G」那样形状的无限循环陷阱（使用沿墙法时会在「G」形迷宫墙壁上不断打转）。
一个仅仅留意著自己目前方向的演算法会陷入一个无穷回圈：以「G」形迷宫为例，一个仅仅留意著自己目前方向（图里的「H」）的演算法会让走迷宫那个人走到最右下角向内凸出来那堵墙时左转，并且走回去左手边那一块那里，然后开始无限打转。普莱吉演算法不会犯这个错，因为在最右下角向内凸出来那堵墙的那一点「总共转的角」不是0度（360度不当0度）， 所以那个人会沿著墙边走到去左下角那一个出口。
该演算法允许拥有指南针的人从任何有限二维迷宫的内部任何点找到前往外部出口的路，而不管从迷宫中的何处才开始依循此演算法，也就是无论从迷宫中的何处作为初始位置都能找到出口。然而，这个演算法在做相反的事情时不起作用，即找到从迷宫外面的入口到迷宫内某个目的地的路。
特雷莫演算法.
特雷莫演算法是由法国数学家发明的一种能有效地找到迷宫出路的方法。这个方法需要在地板上画线来标记路径，只要迷宫有清楚定义了的通道就保证有效。这种做法会把每条通道分成「没去过的」、「去过一次的」、和「去过两次的」三种。并且会接著以下的规则运行：
「转身返回」规则有效地将任何带有循环的迷宫转换为单连通的迷宫；每当我们找到一条可以循环的路径时，我们就将其视为死路并返回。如果没有这条规则，也就是说若遇到迷宫回路时没有回头，而是随意地走另一条路，就有可能切断能进入迷宫中尚未探索部分的通道。
在用这个演算法走到去出口后，只要跟著「仅有做一次记号」的路径当指示就能返回起点。如果迷宫根本没有出口的话，这个方法会带著走迷宫的人回去起点，而且每条路都会有两个记号。在后者这个情况下，每条路恰好走过两次，而两个方向各一次。这个走路过程被人称之为双向双重跟踪（bidirectional double-tracing）。
从本质上讲，这种在19世纪发现的演算法在大约在发现后的一百年后被用作深度优先搜索。
死路填充演算法.
死路填充演算法（dead-end filling）是一个解迷宫演算法，做法如下：把迷宫里的死路全部找出来；再用记号填满所有死路，每条死路填到第一个岔路那里；这样就可以很容易看到整个迷宫里有哪些路是能走的。这种做法可以拿来说明一个完全已知的迷宫，例如是在纸上面玩的迷宫游戏，但是因为这个演算法要求解迷宫那个人能够从迷宫上方鸟瞰看到整个迷宫，所以无法用于未知的迷宫。
死路填充演算法不会意外地切掉从起点到终点的路线，因为演算法的每一个步骤都保留了迷宫的拓朴结构。此外该过程不会提早结束，因为最终的结果不会包含任何死路。因此，如果所解的迷宫是一个完美的迷宫（没有循环回路的迷宫），那么做完死路填充演算法后将会留下迷宫的解，也就是从入口到出口的路线；如果所解的迷宫带有一些循环回路，那么做完死路填充演算法后将会保留迷宫的所有可行解，仅此而已。
递回法.
假如解迷宫的那个人知道整个迷宫的路线（例如是纸上面玩的迷宫游戏），那么透过一个简单的递回演算法就能将这个迷宫从起点走到终点。这个演算法会接收一个起始的座标值：X和Y。如果X和Y的值不在一堵墙上面的话，这个演算法会在所有周围邻近的X和Y值-{zh-hant:呼叫;zh-hans:调用;}-自己，确保-{zh-hant:呼叫;zh-hans:调用;}-的是之前没有使用过的这些X和Y值，而如果他找到终点的X和Y值，那他会以走过那些路以X和Y值的型式记下来一条正确路线。以下是这种演算法用Java程式语言写出来的样本：
int[][] maze = new int[width][height]; //储存整个迷宫的阵列，每个X和Y值都代表迷宫中的一个位置，「1」代表路，「2」代表墙。
boolean[][] wasHere = new boolean[width][height];
boolean[][] correctPath = new boolean[width][height]; // 迷宫解法阵列，等一下用来记下来解迷宫的正确路线。
int startX, startY; //设两个变数做起点的X和Y值。
int endX, endY; //设两个变数做终点的X和Y值。
public void solveMaze() {
 maze = generateMaze(); //产生出整个迷宫，「 1 」代表路，「 2 」代表墙。
 for (int row = 0; row &lt; maze.length; row++) 
 // 将 boolean Arrays 设定为预设值
 for (int col = 0; col &lt; maze[row].length; col++){
 wasHere[row][col] = false;
 correctPath[row][col] = false;
 boolean b = recursiveSolve(startX, startY);
// 会得出一个 Boolean 的阵列（correctPath）
// 其中「真」（true）值会用来代表解迷宫的正确路线。
// 如果b为「假」（false），那就代表个迷宫根本是无解的。
public boolean recursiveSolve(int x, int y) {
 if (x == endX &amp;&amp; y == endY) return true; // 如果到了终点，回传「真」值出去。
 if (maze[x][y] == 2 || wasHere[x][y]) return false; 
// 如果撞到墙或已经来过这一点，回传「假」值出去。
 wasHere[x][y] = true;
 if (x != 0) // 检查一下是不是到了最左的边界。
 if (recursiveSolve(x-1, y)) {
 correctPath[x][y] = true;
 return true;
 if (x != width - 1) // 检查一下是不是到了最右的边界。
 if (recursiveSolve(x+1, y)) {
 correctPath[x][y] = true;
 return true;
 if (y != 0) // 检查一下是不是到了最底的边界。
 if (recursiveSolve(x, y-1)) {
 correctPath[x][y] = true;
 return true;
 if (y != height - 1) // 检查一下是不是到了最顶的边界。
 if (recursiveSolve(x, y+1)) {
 correctPath[x][y] = true;
 return true;
 return false;
铲起迷宫演算法.
铲起迷宫演算法（maze-routing algorithm）是一种用来找出一个迷宫里任意两点之间之路线的方法。这个演算法能够得知两点之间是不是真有路通到，而且无论个迷宫多大都好，他都能让一个从迷宫内部开始走、记忆力有限、事前完全不知道个迷宫是什么样子的个体成功地解完迷宫，只是要求走迷宫的个体记住4个变数就可以找到这条路线出来。但是这个演算法不保证能找到最短路径。
铲起迷宫演算法用了曼哈顿距离（Manhattan distance；简称「MD」）的概念。这个概念指的是两点之间的空间可以用格子代表，而假设一个个体只有沿著格子的边线行走的话，在两点之间穿梭都会有很多条「最短路线」，这些路线的长度就是以所谓的MD计算的。以下是一段虚拟码：
Point src, dst;// 起点和终点的座标
// 「cur」表示现在的位置。
int MD_best = MD(src, dst);//储存走迷宫个体和目的地之间的最短MD。
// 一条好（productive）的路线就是一条能够让MD最小化的路线。
while(cur != dst){
 if(there exists a productive path){
 Take the productive path;
 }else{
 MD_best = MD(cur, dst);
 Imagine a line between cur and dst; // 想像一条目前位置和目的地之间的线。
 Take the first path in the left/right of the line;
 while(MD(cur, dst) != MD_best || there does not exist a productive path) {
 Follow the right-hand/left-hand rule; // 使用沿墙法（左右手法则）。
最短路径演算法.
当迷宫有多个解时，就会希望能找到入口到出口的最短路径。有几种演算法能找到最短路径，其中大部分来自图论。一种方法是使用广度优先搜索来找寻解迷宫的最短路径，而另一种方法——A*搜寻演算法则是使用了启发式的技术。广度优先搜索使用伫列以距离递增的顺序走访迷宫的单元格，每个被走访的单元格都需要追踪它与起点的距离、哪个相邻的单元格更靠近起点导致它被加进伫列中。找到迷宫的终点后，沿著单元格往回找到起点则为最短路径。最简单形式的广度优先搜索有其局限性，例如在加权图中找到最短路径。