算法學習筆記:隨機迷宮


隨機迷宮算法

迷宮生成算法也有很多種,也正是由於結果隨機,反過來講就意味着該問題由無數種解,所以它完全可以利用回溯的思想。

常見算法由三種:

  1. 深度優先算法
  2. 隨機prim算法
  3. 十字分割法

1. 深度優先算法

想象你是一個土撥鼠在挖洞,你不知道往哪個方向挖?這時候應該怎麼做呢?通常來說,你會在這個點做上標記,然後隨機選擇一個方向挖,挖通一堵牆之後,再隨機選擇一個方向,並做上標記,然後繼續挖挖牆。當你挖到了死路,也就是再挖就挖穿了的情況下,你只能往回走,走到上一個標記的岔路口,然後選擇其他的方向繼續挖牆。如此往復,直到你挖遍整個地圖,你回頭一看,哇,一個迷宮就挖好了!

初始迷宮

首先確定標準初始迷宮形式:

  1. 長寬必須爲奇數
  2. 所有的雙奇數座標爲道路,其餘爲牆
    在這裏插入圖片描述

算法

  1. 初始化迷宮,如 初始迷宮 所示,將所有的道路加入未訪問列表
  2. 隨意選擇一個道路作爲出發點,並將之移除未訪問列表
  3. 維護一個分支棧
  4. 只要迷宮中還存在未訪問的道路,進入循環:
    1. 對於當前點,維護一個相鄰點列表。
    2. 搜索當前道路的相鄰道路,如果該帶路未被訪問過,則加入相鄰點列表。
    3. 如果存在這樣的相鄰點:
      1. 將當前道路壓入分支棧
      2. 隨機選擇一個相鄰點
      3. 移除相鄰點與當前點之間的牆壁
      4. 將相鄰點作爲出發點,並將之移除未訪問列表,進入循環。
    4. 如果找不到符合要求的相鄰點
      1. 就意味着此條分支已經走到盡頭
      2. 此時從分支棧當中彈出一個新的元素,進入循環。

代碼

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()

重點

  1. 深度優先算法維護三個列表:
    1. 未訪問列表:保證所有的道路都被訪問
    2. 分支棧:每當你站在一個岔路口,在決定下一個前進方向前,都必須記錄所在的位置,當一個方向走到盡頭,你可以返回這個岔路口,重新選擇下一個方向前進。
    3. 相鄰點列表:決定下一個方向時,可以將所以可能的方向放入這個列表,然後隨機抽取。
  2. 當你逐漸熟悉深度優先算法後,你會發現相鄰點列表不重要,重要的是抽取方式,你可以隨機,也可以一路向北。抽取方式確定了地圖生成的策略
  3. 深度優先算法生成的迷宮只有一條路。

結果

在這裏插入圖片描述

2. 隨機prim算法

  1. 初始化迷宮,如 初始迷宮 所示,將所有的道路加入未訪問列表
  2. 隨意選擇一個道路作爲出發點,並將之移除未訪問列表。
  3. 維護一個分支口列表
  4. 只要迷宮中還存在未訪問的道路,進入循環:
    1. 對於當前點,維護一個相鄰點列表。
    2. 搜索當前道路的相鄰道路,如果該帶路未被訪問過,則加入相鄰點列表。
    3. 如果存在這樣的相鄰點:
      1. 將當前道路加入分支口列表
      2. 隨機選擇一個相鄰點
      3. 移除相鄰點與當前點之間的牆壁
      4. 將相鄰點作爲出發點,並將之移除未訪問列表,進入循環。
    4. 如果找不到符合要求的相鄰點
      1. 就意味着此條分支已經走到盡頭
      2. 此時從分支口列表當中隨機選擇一個元素,進入循環。

初始迷宮

首先確定標準初始迷宮形式:

  1. 長寬必須爲奇數
  2. 所有的雙奇數座標爲道路,其餘爲牆
    在這裏插入圖片描述

代碼

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()

重點

  1. 隨機Prim算法維護三個列表:
    1. 未訪問列表:保證所有的道路都被訪問
    2. 分支列表:每當你站在一個岔路口,在決定下一個前進方向前,都必須記錄所在的位置,當一個方向走到盡頭,你可以從分支列表道中隨機選擇一個岔路口,重新選擇下一個未訪問的方向前進。
    3. 相鄰點列表:決定下一個方向時,可以將所以可能的方向放入這個列表,然後隨機抽取。
  2. 當你逐漸熟悉深度優先算法後,你會發現相鄰點列表不重要,重要的是抽取方式,你可以隨機,也可以一路向北。抽取方式確定了地圖生成的策略
  3. 隨機Prim算法生成的迷宮只有一條路。

結果

在這裏插入圖片描述

深度優先算法與隨機Prim算法的比較

兩種算法代碼結構基本一樣,最關鍵的在於:

  1. 對於深度優先算法來說,當路挖到盡頭,會返回上一個分岔口,從其他方向挖。
  2. 從程序上看,使用的是出棧操作,即最新添加的分支路口擁有最高的優先級。
  3. 對於隨機Prim算法來說,當路挖到盡頭,會隨機從所有的分岔口選擇一個分岔口,然後繼續挖。
  4. 從程序上看,使用的是隨機抽取的方式,即所有分支路口都具有相同的優先級。
  5. 所以兩個算法,一個維護的是棧,一個維護的是列表。

3. 十字分割法

十字分割法非常好理解,就是將一個方形地圖中間橫豎切兩刀,使之成爲 “田” 字型,分析圖形可知,只要將田字當中四面牆的三面打通,就會使得整個地圖當中任意一點能夠連通。
田字形分爲四個象限,每個象限又可以再切兩刀,劃分爲更小的田字,如此往復,可以得到一個迷宮。

初始迷宮

與上面兩種算法不同,十字分割要求初始迷宮如下
在這裏插入圖片描述

算法

  1. 先畫一個十字分成四個部分
  2. 在隨機三面牆上打洞
  3. 再在每個子部分中重複這一步驟,直至空間不夠分割(最小房間)
  4. 一定要注意,橫牆的橫座標,以及豎牆的縱座標必須是偶數

代碼

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()

重點

  1. 十字分割法是遞歸,不是回溯,這裏注意體會一下。
  2. 我們規定整個迷宮最外圍由牆壁包裹,所以迷宮的長與寬必須是奇數,所以最小房間默認情況下是 3 X 3,具體也可根據實際需求自定義,比如 5 X 3,5 X 5 等。
        # 最小的房間大小,3 X 3
        if row2 - row1 < 3 or col2 - col1 < 3:
            return None
    
  3. 橫牆的橫座標必須是偶數,橫牆門的橫座標必須是奇數,豎牆的縱座標必須是偶數,豎牆的縱座標必須是奇數。如何保證座標奇偶性是代碼的難點。
  4. 如果每次遞歸都只在三面牆上打洞,則迷宮走法只有一個解,這在實際應用中意義不大,如果想要有多種走法,降低迷宮難度,則可以考慮有時候在四面牆上打洞,使得迷宮存在迴路。
  5. 十字分割法生成的迷宮會形成一個一個大小不一的房間,適合製作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()

結果

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章