文章目錄
隨機迷宮算法
迷宮生成算法也有很多種,也正是由於結果隨機,反過來講就意味着該問題由無數種解,所以它完全可以利用回溯的思想。
常見算法由三種:
- 深度優先算法
- 隨機prim算法
- 十字分割法
1. 深度優先算法
想象你是一個土撥鼠在挖洞,你不知道往哪個方向挖?這時候應該怎麼做呢?通常來說,你會在這個點做上標記,然後隨機選擇一個方向挖,挖通一堵牆之後,再隨機選擇一個方向,並做上標記,然後繼續挖挖牆。當你挖到了死路,也就是再挖就挖穿了的情況下,你只能往回走,走到上一個標記的岔路口,然後選擇其他的方向繼續挖牆。如此往復,直到你挖遍整個地圖,你回頭一看,哇,一個迷宮就挖好了!
初始迷宮
首先確定標準初始迷宮形式:
- 長寬必須爲奇數
- 所有的雙奇數座標爲道路,其餘爲牆
算法
- 初始化迷宮,如 初始迷宮 所示,將所有的道路加入未訪問列表
- 隨意選擇一個道路作爲出發點,並將之移除未訪問列表
- 維護一個分支棧
- 只要迷宮中還存在未訪問的道路,進入循環:
- 對於當前點,維護一個相鄰點列表。
- 搜索當前道路的相鄰道路,如果該帶路未被訪問過,則加入相鄰點列表。
- 如果存在這樣的相鄰點:
- 將當前道路壓入分支棧
- 隨機選擇一個相鄰點
- 移除相鄰點與當前點之間的牆壁
- 將相鄰點作爲出發點,並將之移除未訪問列表,進入循環。
- 如果找不到符合要求的相鄰點
- 就意味着此條分支已經走到盡頭
- 此時從分支棧當中彈出一個新的元素,進入循環。
代碼
import random
import numpy as np
import matplotlib.pyplot as plt
# 深度優先算法:
# 1. 初始化迷宮,如 **初始迷宮** 所示,將所有的道路加入未訪問列表
# 2. 隨意選擇一個道路作爲出發點,並將之移除未訪問列表
# 3. 維護一個分支棧
# 4. 只要迷宮中還存在未訪問的道路,進入循環:
# 1. 對於當前點,維護一個相鄰點列表。
# 2. 搜索當前道路的相鄰道路,如果該帶路未被訪問過,則加入相鄰點列表。
# 3. 如果存在這樣的相鄰點:
# 1. 將當前道路壓入分支棧
# 2. 隨機選擇一個相鄰點
# 3. 移除相鄰點與當前點之間的牆壁
# 4. 將相鄰點作爲出發點,並將之移除未訪問列表,進入循環。
# 4. 如果找不到符合要求的相鄰點
# 1. 就意味着此條分支已經走到盡頭
# 2. 此時從分支棧當中彈出一個新的元素,進入循環。
row = 31
col = 73
shape = (row, col)
unvisited = []
# 生成矩陣
matrix = np.ones(shape)
for i in range(row):
for j in range(col):
if i % 2 != 0 and j % 2 != 0:
matrix[i, j] = 0
# 未訪問道路列表
unvisited.append((i, j))
# 起點
r, c = 1, 1
# 刪除未訪問
unvisited.remove((r, c))
# 棧
stack = []
# 當還存在未訪問的迷宮單元,進入循環
while unvisited:
# 當前單元必須在矩陣之中,否者退出並從未訪問列表當中移除
if r in range(1, row - 1) and c in range(1, col - 1):
# 搜索當前單元是否有未被訪問過的相鄰單元
neighbour = []
if (r - 2, c) in unvisited:
neighbour.append((r - 2, c))
if (r + 2, c) in unvisited:
neighbour.append((r + 2, c))
if (r, c - 2) in unvisited:
neighbour.append((r, c - 2))
if (r, c + 2) in unvisited:
neighbour.append((r, c + 2))
# 如果有相鄰的單元
if neighbour:
# 將當前迷宮單元入棧
stack.append((r, c))
# 隨機選擇一個相鄰單元
r_new, c_new = random.choice(neighbour)
# 移除當前迷宮單元與相鄰迷宮單元的牆,中間的牆座標就是橫縱座標的平均值
r_wall = int((r + r_new) / 2)
c_wall = int((c + c_new) / 2)
matrix[r_wall, c_wall] = False
# 用相鄰迷宮單元作爲當前迷宮單元
r, c = r_new, c_new
# 從未訪問列表當中移除
unvisited.remove((r, c))
# 如果沒有相鄰單元,意味着該條線路已經挖到底,則試圖返回上一個分支節點,這時需要判斷棧空不空,若不空,則彈出一個元素
elif stack:
# 棧頂的迷宮單元出棧
r, c = stack.pop()
else:
unvisited.remove((r, c))
# 入口
for r in range(1, row - 1):
if matrix[r, 1] == False:
matrix[r, 0] = False
break
# 出口
for r in range(row - 1, 0, -1):
if matrix[r, col - 2] == False:
matrix[r, col - 1] = False
break
# 打印矩陣
plt.matshow(matrix, cmap="binary")
plt.show()
重點
- 深度優先算法維護三個列表:
- 未訪問列表:保證所有的道路都被訪問
- 分支棧:每當你站在一個岔路口,在決定下一個前進方向前,都必須記錄所在的位置,當一個方向走到盡頭,你可以返回這個岔路口,重新選擇下一個方向前進。
- 相鄰點列表:決定下一個方向時,可以將所以可能的方向放入這個列表,然後隨機抽取。
- 當你逐漸熟悉深度優先算法後,你會發現相鄰點列表不重要,重要的是抽取方式,你可以隨機,也可以一路向北。抽取方式確定了地圖生成的策略
- 深度優先算法生成的迷宮只有一條路。
結果
2. 隨機prim算法
- 初始化迷宮,如 初始迷宮 所示,將所有的道路加入未訪問列表
- 隨意選擇一個道路作爲出發點,並將之移除未訪問列表。
- 維護一個分支口列表。
- 只要迷宮中還存在未訪問的道路,進入循環:
- 對於當前點,維護一個相鄰點列表。
- 搜索當前道路的相鄰道路,如果該帶路未被訪問過,則加入相鄰點列表。
- 如果存在這樣的相鄰點:
- 將當前道路加入分支口列表
- 隨機選擇一個相鄰點
- 移除相鄰點與當前點之間的牆壁
- 將相鄰點作爲出發點,並將之移除未訪問列表,進入循環。
- 如果找不到符合要求的相鄰點
- 就意味着此條分支已經走到盡頭
- 此時從分支口列表當中隨機選擇一個元素,進入循環。
初始迷宮
首先確定標準初始迷宮形式:
- 長寬必須爲奇數
- 所有的雙奇數座標爲道路,其餘爲牆
代碼
import random
import numpy as np
import matplotlib.pyplot as plt
# 隨機 Prim 算法:
# 1. 初始化迷宮,如 **初始迷宮** 所示,將所有的道路加入未訪問列表
# 2. 隨意選擇一個道路作爲出發點,並將之移除未訪問列表。
# 3. 維護一個已訪問列表。
# 4. 只要迷宮中還存在未訪問的道路,進入循環:
# 1. 對於當前點,維護一個相鄰點列表。
# 2. 搜索當前道路的相鄰道路,如果該帶路未被訪問過,則加入相鄰點列表。
# 3. 如果存在這樣的相鄰點:
# 1. 將當前道路加入已訪問列表
# 2. 隨機選擇一個相鄰點
# 3. 移除相鄰點與當前點之間的牆壁
# 4. 將相鄰點作爲出發點,並將之移除未訪問列表,進入循環。
# 4. 如果找不到符合要求的相鄰點
# 1. 就意味着此條分支已經走到盡頭
# 2. 此時從以訪問列表當中隨機選擇一個元素,進入循環。
row = 31
col = 73
shape = (row, col)
unvisited = []
# 生成矩陣
matrix = np.ones(shape)
for i in range(row):
for j in range(col):
if i % 2 != 0 and j % 2 != 0:
matrix[i, j] = 0
# 未訪問道路列表
unvisited.append((i, j))
# 起點
r, c = random.choice(unvisited)
# 刪除未訪問
unvisited.remove((r, c))
# 歷史訪問記錄
history = []
# 當還存在未訪問的迷宮單元,進入循環
while unvisited:
# 當前單元必須在矩陣之中,否者退出並從未訪問列表當中移除
if r in range(1, row - 1) and c in range(1, col - 1):
# 搜索當前單元是否有未被訪問過的相鄰單元
neighbour = []
if (r - 2, c) in unvisited:
neighbour.append((r - 2, c))
if (r + 2, c) in unvisited:
neighbour.append((r + 2, c))
if (r, c - 2) in unvisited:
neighbour.append((r, c - 2))
if (r, c + 2) in unvisited:
neighbour.append((r, c + 2))
# 如果有相鄰的單元
if neighbour:
# 將當前迷宮單元加入歷史訪問記錄
history.append((r, c))
# 隨機選擇一個相鄰單元
r_new, c_new = random.choice(neighbour)
# 移除當前迷宮單元與相鄰迷宮單元的牆,中間的牆座標就是橫縱座標的平均值
r_wall = int((r + r_new) / 2)
c_wall = int((c + c_new) / 2)
matrix[r_wall, c_wall] = False
# 用相鄰迷宮單元作爲當前迷宮單元
r, c = r_new, c_new
# 從未訪問列表當中移除
unvisited.remove((r, c))
# 如果沒有相鄰單元,意味着該條線路已經挖到底,則試圖返回上一個分支節點,這時需要判斷棧空不空,若不空,則彈出一個元素
elif history:
# 隨機從一個以訪問列表中選一個單元
r, c = random.choice(history)
else:
unvisited.remove((r, c))
# 入口
for r in range(1, row - 1):
if matrix[r, 1] == False:
matrix[r, 0] = False
break
# 出口
for r in range(row - 1, 0, -1):
if matrix[r, col - 2] == False:
matrix[r, col - 1] = False
break
# 打印矩陣
plt.matshow(matrix, cmap="binary")
plt.show()
重點
- 隨機Prim算法維護三個列表:
- 未訪問列表:保證所有的道路都被訪問
- 分支列表:每當你站在一個岔路口,在決定下一個前進方向前,都必須記錄所在的位置,當一個方向走到盡頭,你可以從分支列表道中隨機選擇一個岔路口,重新選擇下一個未訪問的方向前進。
- 相鄰點列表:決定下一個方向時,可以將所以可能的方向放入這個列表,然後隨機抽取。
- 當你逐漸熟悉深度優先算法後,你會發現相鄰點列表不重要,重要的是抽取方式,你可以隨機,也可以一路向北。抽取方式確定了地圖生成的策略
- 隨機Prim算法生成的迷宮只有一條路。
結果
深度優先算法與隨機Prim算法的比較
兩種算法代碼結構基本一樣,最關鍵的在於:
- 對於深度優先算法來說,當路挖到盡頭,會返回上一個分岔口,從其他方向挖。
- 從程序上看,使用的是出棧操作,即最新添加的分支路口擁有最高的優先級。
- 對於隨機Prim算法來說,當路挖到盡頭,會隨機從所有的分岔口選擇一個分岔口,然後繼續挖。
- 從程序上看,使用的是隨機抽取的方式,即所有分支路口都具有相同的優先級。
- 所以兩個算法,一個維護的是棧,一個維護的是列表。
3. 十字分割法
十字分割法非常好理解,就是將一個方形地圖中間橫豎切兩刀,使之成爲 “田” 字型,分析圖形可知,只要將田字當中四面牆的三面打通,就會使得整個地圖當中任意一點能夠連通。
田字形分爲四個象限,每個象限又可以再切兩刀,劃分爲更小的田字,如此往復,可以得到一個迷宮。
初始迷宮
與上面兩種算法不同,十字分割要求初始迷宮如下
算法
- 先畫一個十字分成四個部分
- 在隨機三面牆上打洞
- 再在每個子部分中重複這一步驟,直至空間不夠分割(最小房間)
- 一定要注意,橫牆的橫座標,以及豎牆的縱座標必須是偶數
代碼
import random
import numpy as np
import matplotlib.pyplot as plt
# 十字分割法
# 採用遞歸方式實現,隨機畫橫豎兩條線,然後在線上隨機開門
# 1. 就是把空間用十字分成四個子空間
# 2. 然後在三面牆上挖洞(爲了確保連通)
# 3. 之後對每個子空間繼續做這件事直到空間不足以繼續分割爲止(最小房間)。
# 牆的座標只能是偶數,路的座標只能是奇數,如何保證奇偶是本算法的難點
def openDoor(x1, y1, x2, y2):
# 橫門的縱座標必須是奇數
if x1 == x2:
pos = y1
while pos % 2 == 0:
pos = random.randint(y1, y2)
maze[x1, pos] = 0
# 縱向開門,門在奇數座標
if y1 == y2:
pos = x1
while pos % 2 == 0:
pos = random.randint(x1, x2)
maze[pos, y1] = 0
def drawMaze(row1, col1, row2, col2):
# 最小的房間大小,3 X 3
if row2 - row1 < 3 or col2 - col1 < 3:
return None
# 橫牆的橫座標必須是偶數
row3 = row1
while row3 % 2 != 0 or row3 == row1 or row3 == row2:
row3 = random.randint(row1, row2)
for i in range(col1, col2 + 1):
maze[row3, i] = 1
# 豎牆的縱座標必須是偶數
col3 = col1
while col3 % 2 != 0 or col3 == col1 or col3 == col2:
col3 = random.randint(col1, col2)
for i in range(row1, row2 + 1):
maze[i, col3] = 1
# 隨機給牆開門。
# 分析圖形可得出結論,對於一個十字型迷宮,只要打開3面牆,則迷宮任意一點可以連通
f = random.randint(1, 4)
if f == 1:
# openDoor(row3, col1, row3, col3) # left
openDoor(row3, col3, row3, col2) # right
openDoor(row1, col3, row3, col3) # up
openDoor(row3, col3, row2, col3) # down
elif f == 2:
openDoor(row3, col1, row3, col3) # left
# openDoor(row3, col3, row3, col2) # right
openDoor(row1, col3, row3, col3) # up
openDoor(row3, col3, row2, col3) # down
elif f == 3:
openDoor(row3, col1, row3, col3) # left
openDoor(row3, col3, row3, col2) # right
# openDoor(row1, col3, row3, col3) # up
openDoor(row3, col3, row2, col3) # down
elif f == 4:
openDoor(row3, col1, row3, col3) # left
openDoor(row3, col3, row3, col2) # right
openDoor(row1, col3, row3, col3) # up
# openDoor(row3, col3, row2, col3) # down
# 遞歸
# 第一象限
drawMaze(row1, col3, row3, col2)
# 第二象限
drawMaze(row1, col1, row3, col3)
# 第三象限
drawMaze(row3, col1, row2, col3)
# 第四象限
drawMaze(row3, col3, row2, col2)
row = 31
col = 73
shape = (row, col)
# 生成初始矩陣
maze = np.zeros(shape)
# 將整個迷宮外圍圍起來
for i in range(col):
maze[0, i] = 1
maze[row - 1, i] = 1
for i in range(row):
maze[i, 0] = 1
maze[i, col - 1] = 1
# 入口
maze[1, 0] = 0
# 出口
maze[row - 2, col - 1] = 0
# 畫圖
drawMaze(1, 1, row - 1, col - 1)
# 顯示圖形
plt.matshow(maze, cmap="binary")
plt.show()
重點
- 十字分割法是遞歸,不是回溯,這裏注意體會一下。
- 我們規定整個迷宮最外圍由牆壁包裹,所以迷宮的長與寬必須是奇數,所以最小房間默認情況下是 3 X 3,具體也可根據實際需求自定義,比如 5 X 3,5 X 5 等。
# 最小的房間大小,3 X 3 if row2 - row1 < 3 or col2 - col1 < 3: return None
- 橫牆的橫座標必須是偶數,橫牆門的橫座標必須是奇數,豎牆的縱座標必須是偶數,豎牆的縱座標必須是奇數。如何保證座標奇偶性是代碼的難點。
- 如果每次遞歸都只在三面牆上打洞,則迷宮走法只有一個解,這在實際應用中意義不大,如果想要有多種走法,降低迷宮難度,則可以考慮有時候在四面牆上打洞,使得迷宮存在迴路。
- 十字分割法生成的迷宮會形成一個一個大小不一的房間,適合製作RPG遊戲地圖。
結果
最小房間:3X3
最小房間:5X5
4. 迷宮走法:深度優先
迷宮走法和生成方法類似,這裏簡單講一下深度優先的走法:
代碼
import random
import numpy as np
import matplotlib.pyplot as plt
m = [
[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4],
[4, 0, 4, 4, 4, 4, 4, 0, 4, 0, 4],
[4, 0, 4, 0, 0, 0, 0, 0, 4, 0, 4],
[4, 0, 4, 4, 4, 4, 4, 0, 4, 4, 4],
[4, 0, 4, 0, 0, 0, 4, 0, 0, 0, 4],
[4, 0, 4, 0, 4, 0, 4, 4, 4, 0, 4],
[4, 0, 4, 0, 4, 0, 0, 0, 0, 0, 4],
[4, 0, 4, 4, 4, 4, 4, 0, 4, 4, 4],
[4, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0],
[4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
]
matrix = np.array(m)
row = 11
col = 11
# 入口
r, c = 1, 0
matrix[r, c] = 2
# 足跡
history = []
history.append((r, c))
stack = []
# 只要沒到出口,就繼續
while r in range(row - 1) and c in range(col - 1):
# 搜索相鄰未探索道路
road = []
if (r, c + 1) not in history and matrix[r, c + 1] == 0:
road.append((r, c + 1))
if (r + 1, c) not in history and matrix[r + 1, c] == 0:
road.append((r + 1, c))
if (r, c - 1) not in history and matrix[r, c - 1] == 0:
road.append((r, c - 1))
if (r - 1, c) not in history and matrix[r - 1, c] == 0:
road.append((r - 1, c))
# 如果有
if road:
stack.append((r, c))
r, c = road.pop(0)
matrix[r, c] = 2
history.append((r, c))
else:
r, c = stack.pop()
# 打印矩陣
plt.matshow(matrix, cmap="binary")
plt.show()