米諾斯迷宮的傳說來源於克里特神話,在希臘神話中也有大量的描述,號稱世界四大迷宮之一。
米諾斯是宙斯和歐羅巴的兒子,因智慧和公正而聞名,死后成為了冥國的判官。由於米諾斯得罪了海神波塞冬,波塞冬便以神力使米諾斯的妻子帕西法厄愛上了一頭公牛,生下了一個牛首人身的怪物米諾陶洛斯。這個半人半牛的怪物不吃其他食物,只吃人肉,因此米諾斯把他關進一座迷宮中,令它無法危害人間。
后來雅典人殺死了米諾斯的一個兒子,為了復仇,米諾斯懇求宙斯的幫助。宙斯給雅典帶來了瘟疫,為了阻止瘟疫的流行,雅典從必須每年選送七對童男童女去供奉怪物米諾陶洛斯。
當雅典第三次納貢時,王子忒修斯自願充當祭品,以便伺機殺掉怪物,為民除害。當勇敢的王子離開王宮時,他對自己的父親說,如果他勝利了,船返航時便會掛上白帆,反之則還是黑帆。忒修斯到了米諾斯王宮,公主艾麗阿德涅對他一見鍾情,並送他一團線球和一柄魔劍,叫他將線頭系在入口處,放線進入迷宮。忒修斯在迷宮深處找到了米諾陶洛斯,經過一場殊死搏斗,終於將其殺死。
忒修斯帶着深愛他的艾麗阿德涅公主返回雅典,卻在途中把她拋在一座孤島上。由於他這一背信棄義的行為,他遭到了懲罰——勝利的喜悅沖昏了他的頭腦,他居然忘記更換船上的黑帆!結果,站在海邊遙望他歸來的父親看到那黑帆之后,認為兒子死掉了,便悲痛地投海而死。

似乎我很小的時候就聽過這個故事,隨着時間的流逝,故事的梗概早已忘卻,但那個神奇的迷宮卻至今都記憶猶新。雖然不清楚當時的迷宮是怎樣設計的,但是我們可以通過遞歸的方法讓米諾斯的迷宮重現人間。
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位的

