【算法】自动生成迷宫

2,156 阅读6分钟

在介绍算法之前,先声明一下迷宫的设计。因为迷宫可以设计成每一块是地面还是墙,也可以设计成每一块周围有四面墙。我们这里就是后者。

那5x5的迷宫来看的话,就是这样的效果:

Prim算法

如果是说自动生成的迷宫要有一定的难度,那首选肯定是这种算法。

其实这个算法本来是用来得到最小生成树的,是一个图算法。简单来说就是几个点,两两相连,然后每条连线的权重不同,可以看作距离或者通信损耗等。

这个算法可以让我们找到整个图中,相加权重最小的一个路径,也就是最小生成树。

我们把这个算法稍微演变一下,就可以使用在迷宫生成上了,因为本质上迷宫每个点就是图上的点,然后每堵墙就是点和点的连线。设计迷宫,就是通过打通若干墙壁,使得每个点都可以走到迷宫上的另外任意一个点

设计思路

这种算法的初始化就是想上图所示,需要每个墙都先关上,然后我们后面进行“敲墙”的工作。

对于一个这样的二维迷宫,按照Prim算法,可以设计如下步骤:

  1. 我们首先任意寻找一个点,作为起点 (x, y)

  2. 然后上下左右任意找一个相邻的点(当然不能出框),比如(x+1, y)

  3. 判断两个点之间在不在同一条路径上

    如果已经在同一条路径上了,就什么都不做。

    如果不在同一条路径上,则打通中间这面墙。

  4. 重复1,2,3步骤,直到敲掉的墙的数量等于 sizeX * sizeY - 1

我们可以看到,上面最关键的就是第三步,判断两个点是否已经在同一路径上,和打通这面墙。

这就很容易想到一个非常契合的数据结构:并查集。

并查集(Union Find)

这是整个算法的关键所在。并查集其实是一个树形的数据结构,树和树之间可以合并,并且每个子节点可以找到根节点的所在。

什么是根节点?

在树形结构中,每个子节点会指向父节点,父节点再指向它的父节点,最后,根节点就是自己指向自己的节点

应用到迷宫这里,我们可以把一条连通的路径转换成一个树形结构,这样,两个子节点可以通过查找根节点是否相同,来判断他们俩在不在同一条通路上。如果根节点不同,则说明不在同一个通路上,那么就需要砸墙。

砸墙这个步骤,恰恰就是两个树进行合并的一个过程

什么是合并?

因为两个合并的树可能都会很庞大,所以其实合并过程不需要遍历整个树。只要把A树的根节点,指向B树的根节点,就可以完美合并两个树了。新的树的根节点就是B树原来的根节点,A树的原来的所有子节点,也可以通过A树的原来的根节点,找到合并后的根节点。

这样凭空说可能有点难以理解,上图!

这里以4x4的迷宫作为例子,我们首先建立一个可以实现树形结构的一个二维数组。初始化时,每一个元素都存储着自己的坐标。

0123
00,00,10,20,3
11,01,11,21,3
22,02,12,22,3
33,03,13,23,3

比如我们首先选定(0,2)和(0,3),因为他们自己都是根节点,显然根节点不同,打通,并且合并两个根节点。我们这里随机取(0,3)为新的根节点,就变成了:

0123
00,00,10,30,3
11,01,11,21,3
22,02,12,22,3
33,03,13,23,3

以此类推,我们每次都只要随机找两个点,来做上面的步骤就行了。

那么接下来看一个两个相对大的树的合并。

0123
00,00,10,10,2
10,01,01,11,2
22,02,12,22,3
33,03,13,23,3

上面这张表,我们可以看到有两条通路,一个是根节点在(0,0),一个L型通道点(1,3)上。另一条是从(0,1)通向(0,3)。

现在假设选中了点(0,3)和(1,3),通过查找可以发现不是同一个根节点,于是就把墙打通。同时,把根节点连到另一个根节点上。变成:

0123
00,00,00,10,2
10,01,01,11,2
22,02,12,22,3
33,03,13,23,3

注意,这边其实只修改了一个单元格,把(0,1)这个里面的内容从(0,1)改成(0,0),其他任何单元格不需要做任何修改

打通墙壁之后,这个迷宫就画成了这样:

路径压缩

上面的这一系列操作,你可能会发现一个问题,如果这个迷宫很大很大,迷宫错综复杂,那每个节点要找到他的根节点,就要花很久。因为是通过父节点一步一步去找的,会使整个算法的效率随着迷宫规模的扩大,而明显下降。

其实,我们整个过程中,只要需要找到根节点,和父节点的关系其实不是很大。

所以,本质上,每个单元格要存的,其实不一定要是父节点的坐标,而只要根节点的坐标,就能完全满足我们的需求了。

对,这就是路径压缩!

还是接着上面的那个例子,我们只要多加一个步骤,就是每次加入子节点的时候,直接存根节点的坐标就行了。合并前的表格就会变成:

0123
00,00,10,10,1
10,00,00,00,0
22,02,12,22,3
33,03,13,23,3

合并之后,也一样。

如果迷宫规模不是特别大的话,我们可以接受适当的父节点的数量,那么只要和之前一样,替换根节点的指向就行了:

0123
00,00,00,10,1
10,00,00,00,0
22,02,12,22,3
33,03,13,23,3

如果严格地做完全压缩的话,保证树最多只有两层,那就需要遍历一下,修改所有的点:

0123
00,00,00,00,0
10,00,00,00,0
22,02,12,22,3
33,03,13,23,3

压缩完之后,对于超大迷宫,效率上会有一定的提升!

代码实现之后,生成效果如下(14x18的迷宫):