接上文的介绍,本文将主要介绍如何生成随机迷宫,在网上找到的资源也比较多,这里我选取了随机 Prim 算法生成迷宫,选择这个算法的理由如下:
算法思想简单,易于实现
生成的迷宫比较自然,不会出现明显的主路
接下来本文将对该算法进行介绍,如果对上一篇推文《数据结构课程设计——迷宫求解(一)》中的可视化不够理解,也没有关系,可视化只是为了结果演示,仅是一个可选项。
0 1Prim 算法在开始讲解随机 Prim 算法之前,先来一个题外话,介绍下图论中关于最小生成树的 Prim 算法,便于接下来理解迷宫生成的过程。
下图为一个无向带权图,假定我们先选取了 A 点作为出发点。
从 A 点出发可以有两条路,分别走向 B 和 C ,这时候选择走哪一条就比较重要了,我们希望最后生成树的边上权值之和最小,所以这里我们选择了 B,AB 之间的权值是 3,比 AC 之间的权值 4 更小。
选择了 B 点之后,我们现在有了 A、B 两个点,可以走的边还有 AD、BC、BE,从中继续选出权值最小的边,这次选择了 AD 边。接下来继续考虑 A、B、D 三个点向外可以走的边,每次新加一个点就会新增一些边,考虑的时候总是只选择权值最小的边,而且选择的这条边必须要包含一个未曾访问过的顶点,直到所有的顶点都已经被考虑过了才结束。
最终的生成过程如下图所示。
0 2随机 Prim 算法生成迷宫最小生成树的 Prim 算法介绍结束了,接下来就是生成迷宫的重点了。
先说说二者在实现上的区别,在讲到 Prim 算法构造最小生成树的时候,每一次选哪条路径都是有明确目的的,即每次都选权值最小的那条边来向外扩展,现在我们构造迷宫,要保证迷宫是随机生成的,那么就不能够有明确的选择,只能是随机选择。
而和 Prim 算法构造最小生成树类似的是,都是从一个点开始,不停的向外扩展,直到没有可以扩展的点为止。
接下来就看看该如何进行生成,首先肯定得有一个已知的图,我们在这个图上进行生成迷宫,我们使用二维数组表示一个平面的迷宫,其中数字 1 代表墙壁,数字 0 代表地面,假设这个二维数组行列均为 15。实际上根据自己的需要进行设定即可,或者也可以设置不同的难度等级,这里留给读者自行思考,不过需要设置成奇数,否则会出现某一侧的墙是两层这种情况。
初始化代码如下,接下来只要在里面加生成随机迷宫的方法即可
public class RandomMaze {int row; // 行int col; // 列int maze[][]; // 二维数组,用于表示迷宫
String color[][]; // 用于记录各点颜色,在随机 Prim 算法中会用到,当然也可以使用其他类型来做一个标记// 初始化,这里直接固定行列值为 21public RandomMaze(){this.row = 15;this.col = 15;this.maze = new int[row][col];this.color = new String[row][col];
}
}
随机 Prim 算法生成迷宫之前,需要先对迷宫进行初始化为全 1,也就是全部都是墙壁,然后在其中的奇数行且奇数列进行“挖洞”,也就是将该位置的 1 改成 0,这样就形成了一个 0 被 1 包围的局面,并将墙壁标记为灰色“gray”,地面标记为黄色“yellow”。代码和示意图如下。
// 初始化迷宫矩阵
public void initMaze(){
// 先全部赋值为 1
for(int i=0; i for (int j = 0; j this.maze[i][j] = 1;
this.color[i][j] = "gray"; // 墙壁标记为灰色
}
// 对奇数行且奇数列进行 “挖洞”
for(int i=0; i2; i++)for (int j = 0; j 2; j++) {this.maze[2*i+1][2*j+1] = 0;this.color[2*i+1][2*j+1] = "yellow"; // “挖洞” 挖出来的 0 标记为黄色
}
}
有了这个挖好洞的迷宫,接下来选一个黄色的 0 ,并将它改成红色的 0,同时,将它的四周墙壁从灰色的 1 变成蓝色的 1,使用随机数,任意抽取一个蓝色的 1,与这个红色 0 只隔着这个选中的蓝色 1 的格子如果是黄色的 0,那么就把这堵墙“打通”,即将选中的这个蓝色 1 改成 0 。同时把新加入的 黄色 0 改成红色,把它周围的墙壁变成蓝色。这一步与最小生成树中逐步向外扩展很类似,只是这里使用了颜色区分。
这里我们直接使用第 1 行第 1 列的 0 作为起点 (整个矩阵由于是数组,所以下标从 0 开始)。
假设随机数选中了右侧的这个蓝色 1,接下来如下图所示
打通墙壁就是将蓝色的 1 变为 0,如果选择的一个蓝色的 1 对面没有黄色的 0,例如我们选择了最上方的蓝色 1,显然对面没有黄色 0,这时候将蓝色 改回 灰色即可。
不断重复上述过程,直到整个迷宫中没有任何一个蓝色的 1 时结束,生成完成。
关于为什么要将黄色的 0 改成红色,这里我只公布答案,如果不改,最终生成的迷宫地图如下。对此感兴趣的读者可以思考下为什么会出现这种情况。
思路介绍完了就直接贴出代码了。
// 创建随机迷宫
public void createMaze(){
Random rand = new Random();
ArrayList list = new ArrayList<>();
int start_x = 1, start_y = 1; // 起始点为第 1 行第 1 列
boolean moved = true; // 是否成功添加一个黄色的 0
int length;
int index;
this.color[start_x][start_y] = "red"; // 先将当前黄色 0 改成红色
while(true){ // 死循环,等待内部break
if(moved) { // 如果有移动,则将移动后新加结点的周围墙壁也添加进入列表
if (start_x + 1 this.row && this.maze[start_x + 1][start_y] == 1) {
//this.color[start_x+1][start_y] = "blue"; // 将墙的颜色改为蓝色
list.add(new Node(start_x + 1, start_y, "Down"));
}
if (start_x - 1 >= 0 && this.maze[start_x - 1][start_y] == 1) {
list.add(new Node(start_x - 1, start_y, "Up"));
}
if (start_y + 1 this.col && this.maze[start_x][start_y + 1] == 1) {
list.add(new Node(start_x, start_y + 1, "Right"));
}
if (start_y - 1 >= 0 && this.maze[start_x][start_y - 1] == 1) {
list.add(new Node(start_x, start_y - 1, "Left"));
}
}
length = list.size();
if(length==0) break; // list 为空时结束循环
index = rand.nextInt(length); // 随机获得一个结点
Node node = (Node)list.remove(index); // 移除该结点
start_x = node.x; // 以该结点为新的起始点
start_y = node.y;
moved = false; // 尚未移动
// 四个判断条件分别为移动方向、边界、是否访问过、是否有路可走
if(node.direction=="Up" && start_x-1>=0 && this.maze[start_x-1][start_y]==0 && this.color[start_x-1][start_y]=="yellow"){ // 试图上移
this.maze[start_x][start_y] = 0; // 打通墙壁
start_x = start_x - 1;
this.color[start_x][start_y] = "red";
moved = true;
}
if(node.direction=="Down" && start_x+1<this.row && this.maze[start_x+1][start_y]==0 && this.color[start_x+1][start_y]=="yellow"){ // 试图下移
this.maze[start_x][start_y] = 0;
start_x = start_x + 1;
this.color[start_x][start_y] = "red";
moved = true;
}
if(node.direction=="Left" && start_y-1>=0 && this.maze[start_x][start_y-1]==0 && this.color[start_x][start_y-1]=="yellow"){ // 试图左移
this.maze[start_x][start_y] = 0;
start_y = start_y - 1;
this.color[start_x][start_y] = "red";
moved = true;
}
if(node.direction=="Right" && start_y+1<this.col && this.maze[start_x][start_y+1]==0 && this.color[start_x][start_y+1]=="yellow"){ // 试图右移
this.maze[start_x][start_y] = 0;
start_y = start_y + 1;
this.color[start_x][start_y] = "red";
moved = true;
}
}
}
代码当中用到了一句 new Node,这是在创建 Node 类的对象,Node 类代码稍后展示,它就相当于是一个三元组,存储着 。这个移动方向很关键,当我们随机选中一个墙壁时候,能够知道它的“对面”到底在哪里,或者说是往哪个方向走才是对面就是靠的这个移动方向。
代码中能够看到,关于把墙壁改成蓝色这一步,我注释掉了,并且说明了可以不用写,原因也很简单,这里将墙壁这个三元组放入 list 中,就可以代替改成蓝色了,而且之后有从 list 中移除随机选中的三元组这一步,这可以替代如果“打通失败”时候改回灰色这一步,即 list 中存储的就已经代表了是 蓝色的 1 了,移除就代表该选中的 1 已经进行过处理了,即,将该选中的 1 改成 0 或者恢复为 灰色 1。
每次从 list 中随机抽取一个墙壁,需要看沿着它的移动方向,它的“对面”是否越界,以及是否为 黄色的 0,然后才能决定是否要“打通这堵墙”。如果打通了,要将新加入的点的四周墙壁尝试添加到 list 中。
下面附上 Node 类的代码,如果学过 Java 应该能够看出来,这是写在另一个 Java 文件中的。
public class Node {
int x; // x 坐标
int y; // y 坐标
String direction; // 接下来的移动方向
public Node(int x, int y, String direction){
this.x = x;
this.y = y;
this.direction = direction;
}
}
现在已经完成了随机生成迷宫的算法,可以在该工程中创建一个文件测试这些方法了,如果能够使用图形界面展示就更直观一点,这里我直接使用了图形界面,上一篇推文介绍过,并不难实现,此处不多解释。
直接另设一个方法,遍历矩阵,数字 1 就画成黑色矩形代表墙壁,数字 0 就画成白色矩形代表地面。某次生成的迷宫如下图所示。
如果对图形界面不感兴趣或者不熟悉,也可以直接将矩阵以方阵形式打印显示出来。
END在最后Last but not least
本文接上一篇推文内容,利用随机 Prim 算法生成迷宫,简单介绍了随机 Prim 算法的思想,如果学过图论,可以对照着图论中求最小生成树的 Prim 算法进行学习,算法中涉及到修改颜色的部分可能会比较难理解,可以在纸上画图辅助理解。
本文已经实现了生成随机迷宫,下一篇推文将开始介绍如何在一个随机生成的迷宫中从入口出发找到一条通往出口的路。
往期 精彩回顾Python数据结构——树(四)
Python数据结构——树(五)
数据结构课程设计——迷宫求解(一)