遞歸的邏輯(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位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享為主,如需轉載,請聯系本人,標明作者和出處,非商業用途! 

  掃描二維碼關注公眾號“我是8位的”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM