递归的逻辑(5)——米诺斯的迷宫
米诺斯迷宫的传说来源于克里特神话,在希腊神话中也有大量的描述,号称世界四大迷宫之一。
米诺斯是宙斯和欧罗巴的儿子,因智慧和公正而闻名,死后成为了冥国的判官。由于米诺斯得罪了海神波塞冬,波塞冬便以神力使米诺斯的妻子帕西法厄爱上了一头公牛,生下了一个牛首人身的怪物米诺陶洛斯。这个半人半牛的怪物不吃其他食物,只吃人肉,因此米诺斯把他关进一座迷宫中,令它无法危害人间。
后来雅典人杀死了米诺斯的一个儿子,为了复仇,米诺斯恳求宙斯的帮助。宙斯给雅典带来了瘟疫,为了阻止瘟疫的流行,雅典从必须每年选送七对童男童女去供奉怪物米诺陶洛斯。
当雅典第三次纳贡时,王子忒修斯自愿充当祭品,以便伺机杀掉怪物,为民除害。当勇敢的王子离开王宫时,他对自己的父亲说,如果他胜利了,船返航时便会挂上白帆,反之则还是黑帆。忒修斯到了米诺斯王宫,公主艾丽阿德涅对他一见钟情,并送他一团线球和一柄魔剑,叫他将线头系在入口处,放线进入迷宫。忒修斯在迷宫深处找到了米诺陶洛斯,经过一场殊死搏斗,终于将其杀死。
忒修斯带着深爱他的艾丽阿德涅公主返回雅典,却在途中把她抛在一座孤岛上。由于他这一背信弃义的行为,他遭到了惩罚——胜利的喜悦冲昏了他的头脑,他居然忘记更换船上的黑帆!结果,站在海边遥望他归来的父亲看到那黑帆之后,认为儿子死掉了,便悲痛地投海而死。
似乎我很小的时候就听过这个故事,随着时间的流逝,故事的梗概早已忘却,但那个神奇的迷宫却至今都记忆犹新。虽然不清楚当时的迷宫是怎样设计的,但是我们可以通过递归的方法让米诺斯的迷宫重现人间。
1 # 迷宫矩阵 2 maze = [ 3 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 4 [0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1], 5 [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1], 6 [1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1], 7 [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1], 8 [1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1], 9 [1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1], 10 [1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1], 11 [1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1], 12 [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1], 13 [1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1], 14 [1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1], 15 [1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1], 16 [1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1], 17 [1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], 18 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 19 ] 20 def paint(maze): 21 ''' 打印迷宫 ''' 22 for a in maze: 23 for i in a: 24 print('%4d' % i, end='') 25 print() 26 27 if __name__ == '__main__': 28 paint(maze)
在矩阵中,用0表示通道,1表示墙壁,忒修斯王子可以在0之间任意穿行,矩阵迷宫的打印结果:
迷宫的数据结构
虽然可以用0和1绘制出一个迷宫,但仍然属于手动编辑,我们的目标是寄希望于计算机,自动生成并绘制一个大型的迷宫:
迷宫中有很多墙壁,再用0和1组成的简单矩阵就不合适了。如果将迷宫矩阵的每一个位置看作一个方块,则方块的上、下、左、右都可能有墙壁存在,这就需要对每个位置记录四面墙壁的信息:
实际上没那么复杂,只要记录上墙和右墙就可以了,至于下墙和左墙,完全可以由相邻方块的上墙和右墙代替:
当然,最后还要在四周套上一层边框:
在生成迷宫时,每一个方块都需要记录三种信息:是否已经被设置、是否有右墙、是否有上墙。一个较为“面向对象”的方法是将方块信息设计成一个类结构,用三个布尔型属性来记录信息,但是这样做性价比并不高,一种更简单且高效的方式是用一个3位的二进制数来表示:
矩阵中所有元素的初始值都设置为011,也就是方块未设置、有右墙和上墙;如果已经设置了某个方块,那么第3位被置为1,如此一来,每个方块可能会有5种状态:
使用下面的代码设置一个8×8迷宫矩阵的初始状态:
1 # 迷宫矩阵 2 class MinosMaze: 3 maze = [] # 迷宫矩阵 4 n = 0 # 矩阵维度 5 init_status = 0b011 # 初始状态,有上墙和右墙 6 def __init__(self, n: int): 7 ''' 初始化一个 n 维的迷宫 ''' 8 self.n = n 9 # 初始化迷宫矩阵,所有方块未设置、有右墙、有上墙 10 self.maze = [([self.init_status] * n) for i in range(n)] 11 12 def patin_maze(self): 13 for a in self.maze: 14 for i in a: 15 print('%4d' % i, end='') 16 print() 17 18 if __name__ == '__main__': 19 m = MinosMaze(8) 20 m.patin_maze()
我们使用拆墙法自动生成迷宫,这需要遍历迷宫矩阵中的每一个方格,设置是否拆除右墙或上墙。用递归的方法随机遍历上下左右四个方向,直到所有方向全部遍历完为止:
四个的拆墙过程如下:
1. 向上遍历,需要拆除当前方格的上墙;
2. 向下遍历,需要拆除下侧方格的上墙;
3. 向左遍历,需要拆除左侧方格的右墙;
4. 向右遍历,需要拆除当前单元格的右墙。
1 class MinosMaze: 2 …… 3 def remove_wall(self, i, j, side): 4 ''' 拆掉maze[i][j] 的上墙或右墙 ''' 5 if side == 'U': 6 self.maze[i][j] &= 0b110 # 拆掉上墙 7 elif side == 'R': 8 self.maze[i][j] &= 0b101 # 拆掉右墙
自动生成迷宫
通过递归的方式遍历方格,迷宫矩阵的方格会逐一被设置:
1 import random 2 3 class MinosMaze: 4 ... 5 def create(self): 6 ''' 自动创建迷宫 ''' 7 def auto_create(i, j): 8 self.maze[i][j] |= 0b100 # maze[i][j] 已经被设置过 9 # 当self.maze[i][j]的上下左右四个方向都是初始状态时,开始拆墙操作 10 while (i - 1 >= 0 and self.maze[i - 1][j] == self.init_status) \ 11 or (i + 1 < self.n and self.maze[i + 1][j] == self.init_status) \ 12 or (j - 1 >= 0 and self.maze[i][j - 1] == self.init_status) \ 13 or (j + 1 < self.n and self.maze[i][j + 1] == self.init_status): 14 side = random.choice(['U', 'D', 'L', 'R']) # 随机方向 15 # 能够向↑走 16 if side == 'U' and i - 1 >= 0 and self.maze[i - 1][j] == self.init_status: 17 self.remove_wall(i, j, 'U') # 拆除当前方格的上墙 18 auto_create(i - 1, j) # 向↑走 19 # 能够向↓走 20 elif side == 'D' and i + 1 < self.n and self.maze[i + 1][j] == self.init_status: 21 self.remove_wall(i + 1, j, 'U') # 拆除下侧方格的上墙 22 auto_create(i + 1, j) # 向↓走 23 # 能够向←走 24 elif side == 'L' and j - 1 >= 0 and self.maze[i][j - 1] == self.init_status: 25 self.remove_wall(i, j - 1, 'R') # 拆除左侧方格的右墙 26 auto_create(i, j - 1) # 向←走 27 # 能够向→走 28 elif side == 'R' and j + 1 < self.n and self.maze[i][j + 1] == self.init_status: 29 self.remove_wall(i, j, 'R') # 拆除当前单元格的右墙 30 auto_create(i, j + 1) # 向→走 31 auto_create(0, 0) # 从入口位置开始遍历 32 33 def patin_maze(self): 34 ''' 打印迷宫数组 ''' 35 for a in self.maze: 36 for i in a: 37 print('%4d' % i, end='') 38 print() 39 40 if __name__ == '__main__': 41 m = MinosMaze(8) 42 m.create() 43 m.patin_maze()
程序构造了一个8×8的迷宫,一种可能的结果是:
矩阵元素的打印的结果是十进制整数,它和二进制的对应关系:
画出迷宫
绘制迷宫的方法很简单,只需在坐标轴中画出每个方格的墙壁就好了:
1 import random 2 import matplotlib.pyplot as plt 3 4 class MinosMaze: 5 …… 6 def paint(self): 7 # 绘制迷宫内部 8 for i in range(self.n): 9 for j in range(self.n): 10 # 有右墙 11 if self.maze[i][j] & 0b010 == 0b010: 12 # 右墙的坐标 13 r_x, r_y = [j + 1, j + 1], [self.n - i, self.n - i - 1] 14 plt.plot(r_x, r_y, color='black') 15 # 有上墙 16 if self.maze[i][j] & 0b001 == 0b001: 17 # 上墙的坐标 18 u_x, u_y = [j, j + 1], [self.n - i, self.n - i] 19 plt.plot(u_x, u_y, color='black') 20 21 plt.axis('equal') 22 ax = plt.gca() 23 ax.spines['top'].set_visible(False) 24 ax.spines['right'].set_visible(False) 25 plt.show()
看起来不那么像迷宫,这是由于没有添加边框,因此还需要在paint()方法中加上最后的完善工作:
1 def paint(self): 2 …… 3 plt.plot([0, self.n], [self.n, self.n], color='black') # 上边框 4 plt.plot([0, self.n], [0, 0], color='black') # 下边框 5 plt.plot([0, 0], [0, self.n], color='black') # 左边框 6 plt.plot([self.n, self.n], [0, self.n], color='black') # 右边框 7 8 # 设置入口和出口 9 entrance, exit = ([0, 0], [self.n, self.n - 1]), ([self.n, self.n], [0, 1]) 10 plt.plot(entrance[0], entrance[1], color='white') 11 plt.plot(exit[0], exit[1], color='white') 12 13 plt.axis('equal') 14 ax = plt.gca() 15 ax.spines['top'].set_visible(False) 16 ax.spines['right'].set_visible(False) 17 plt.show()
出口的位置在迷宫的右下角,由于创建迷宫时遍历了所有方格,因此出口方格一定是从它上侧或左侧的方格遍历而来的,这意味着它一定没有上墙或左墙,拆掉它的右边框一定能够成为出口。现在可以终于可以绘制出一个完整的迷宫了:
米诺斯的迷宫复杂的多,也许一个32×32的设计图可以困住怪兽:
以下是完整的代码:
1 import random 2 import matplotlib.pyplot as plt 3 4 # 迷宫矩阵 5 class MinosMaze: 6 ''' 米诺斯迷宫 7 Attributes: 8 maze: 迷宫矩阵 9 n: 矩阵维度 10 init_status: 方格的初始状态 11 ''' 12 maze = [] 13 n = 0 14 init_status = 0b011 # 初始状态,有上墙和右墙 15 16 def __init__(self, n: int): 17 ''' 初始化一个 n 维的迷宫 ''' 18 self.n = n 19 # 初始化迷宫矩阵,所有方块未设置、有右墙、有上墙 20 self.maze = [([self.init_status] * n) for i in range(n)] 21 22 def remove_wall(self, i, j, side): 23 ''' 拆掉maze[i][j] 的上墙或右墙 ''' 24 if side == 'U': 25 # 拆掉上墙 26 self.maze[i][j] &= 0b110 27 elif side == 'R': 28 # 拆掉右墙 29 self.maze[i][j] &= 0b101 30 31 def create(self): 32 ''' 自动创建迷宫 ''' 33 def auto_create(i, j): 34 # maze[i][j] 已经被设置过 35 self.maze[i][j] |= 0b100 36 37 # 当self.maze[i][j]的上下左右四个方向都是初始状态时,开始拆墙操作 38 while (i - 1 >= 0 and self.maze[i - 1][j] == self.init_status) \ 39 or (i + 1 < self.n and self.maze[i + 1][j] == self.init_status) \ 40 or (j - 1 >= 0 and self.maze[i][j - 1] == self.init_status) \ 41 or (j + 1 < self.n and self.maze[i][j + 1] == self.init_status): 42 # 随机方向 43 side = random.choice(['U', 'D', 'L', 'R']) 44 # 能够向↑走 45 if side == 'U' and i - 1 >= 0 and self.maze[i - 1][j] == self.init_status: 46 # 拆除当前方格的上墙 47 self.remove_wall(i , j, 'U') 48 # 向↑走 49 auto_create(i - 1, j) 50 # 能够向↓走 51 elif side == 'D' and i + 1 < self.n and self.maze[i + 1][j] == self.init_status: 52 # 拆除下侧方格的上墙 53 self.remove_wall(i + 1 , j, 'U') 54 # 向↓走 55 auto_create(i + 1, j) 56 # 能够向←走 57 elif side == 'L' and j - 1 >= 0 and self.maze[i][j - 1] == self.init_status: 58 # 拆除左侧方格的右墙 59 self.remove_wall(i, j - 1, 'R') 60 # 向←走 61 auto_create(i, j - 1) 62 # 能够向→走 63 elif side == 'R' and j + 1 < self.n and self.maze[i][j + 1] == self.init_status: 64 # 拆除当前单元格的右墙 65 self.remove_wall(i, j, 'R') 66 # 向→走 67 auto_create(i, j + 1) 68 # 从入口位置开始遍历 69 auto_create(0, 0) 70 71 def patin_maze(self): 72 for a in self.maze: 73 for i in a: 74 print('%4d' % i, end='') 75 print() 76 77 def paint(self): 78 # 绘制迷宫内部 79 for i in range(self.n): 80 for j in range(self.n): 81 # 有右墙 82 if self.maze[i][j] & 0b010 == 0b010: 83 # 右墙的坐标 84 r_x, r_y = [j + 1, j + 1], [self.n - i, self.n - i - 1] 85 plt.plot(r_x, r_y, color='black') 86 # 有上墙 87 if self.maze[i][j] & 0b001 == 0b001: 88 # 上墙的坐标 89 u_x, u_y = [j, j + 1], [self.n - i, self.n - i] 90 plt.plot(u_x, u_y, color='black') 91 92 # 上边框 93 plt.plot([0, self.n], [self.n, self.n], color='black') 94 # 下边框 95 plt.plot([0, self.n], [0, 0], color='black') 96 # 左边框 97 plt.plot([0, 0], [0, self.n], color='black') 98 # 右边框 99 plt.plot([self.n, self.n], [0, self.n], color='black') 100 101 # 设置入口和出口 102 entrance, exit = ([0, 0], [self.n, self. n - 1]), ([self.n, self.n], [0, 1]) 103 plt.plot(entrance[0], entrance[1], color='white') 104 plt.plot(exit[0], exit[1], color='white') 105 106 plt.axis('equal') 107 ax = plt.gca() 108 ax.spines['top'].set_visible(False) 109 ax.spines['right'].set_visible(False) 110 plt.show() 111 112 if __name__ == '__main__': 113 # m = MinosMaze(8) 114 # m = MinosMaze(16) 115 m = MinosMaze(32) 116 m.create() 117 m.patin_maze() 118 m.paint()
作者:我是8位的