1. 程式人生 > 其它 >生成樹演算法生成迷宮

生成樹演算法生成迷宮

生成迷宮的常見演算法有遞歸回溯、遞迴分治等等;生成樹演算法可以用來生成完美迷宮,即任何兩個可達點之間只有一條通路;本文采用`Kruskal最小生成樹演算法`來生成一個完美迷宮

生成迷宮的常見演算法有遞歸回溯、遞迴分治等等;生成樹演算法可以用來生成完美迷宮,即任何兩個可達點之間只有一條通路;本文采用Kruskal最小生成樹演算法生成一個完美迷宮

演算法介紹

Kruskal 演算法

生成樹:對連通圖進行遍歷,過程中所經過的邊和頂點的組合可看做是一棵普通樹,通常稱為生成樹;生成樹必須符合兩個條件:1)包含連通圖中所有的頂點;2)任意兩頂點之間有且僅有一條通路
最小生成樹:無向連通圖邊權和最小的生成樹
可見生成樹的特點和完美迷宮非常契合(可達點之間有且僅有一條通路),所以生成樹演算法都可以用來生成迷宮
Kruskal演算法:一種常見的最小生成樹演算法,該演算法維護一堆集合,查詢兩個元素(節點)是否屬於同一集合,合併兩個集合,直到形成一棵樹

#### Kruskal 演算法描述 ####
1. 輸入:一個加權連通圖,其中頂點集合為V,邊集合為E
2. 初始化:E'為新的邊集,初始為空
3. 排序E,依次取E中最小邊,若兩邊節點不屬於同一集合,則合併節點,將邊加入E';否則忽略
4. 重複 步驟3 知道E'中邊數量達到n-1(節點總數為n),則生成樹完成
5. 使用E'描述最小生成樹

並查集

並查集:一種屬性資料結構,支援兩種操作:1)確定某個元素處於哪個子集;2)將兩個子集合併成一個集合
關於並查集,有個廣為流傳的形象比喻:幾個家族進行宴會(也有說門派的),但是家族普遍長壽,所以人數眾多。由於長時間的分離以及年齡的增長,這些人逐漸忘掉了自己的親人,只記得自己的爸爸是誰了,而最長者(稱為「祖先」)的父親已經去世,他只知道自己是祖先。為了確定自己是哪個家族,他們想出了一個辦法,只要問自己的爸爸是不是祖先,一層一層的向上問,直到問到祖先。如果要判斷兩人是否在同一家族,只要看兩人的祖先是不是同一人就可以了
並查集的思想就類似於此,一層層向上查詢
路徑壓縮

:很明顯,這裡存在一個問題:當家族鏈過長時,一層層向上查詢祖先是誰明顯非常浪費時間;這裡維護了父親輩、爺爺輩這些跟祖先沒有關係的資訊,完全可以忽略,只需要記錄某人的祖先是誰即可;這樣,原本高度很高的樹就變成了一顆高度為2的樹了
合併操作:合併操作很簡單,例如合併A和B,因為並不在意合併後的祖先是fa還是fb,只要有一個即可,所以只需要把其中一個集合掛到另一個幾個下面即可;某些情況下,合併時子集節點地數量可能會影響效率,這時候可以考慮按秩合併,即按照A和B的節點數或者樹高等因素來選擇最後祖先是fa還是fb

''' 參考實現 '''
fa = [0] * MAXN                  # 記錄某個人的爸爸是誰,特別規定,祖先的爸爸是他自己

# 不帶路徑壓縮
def find(x):
    if fa[x] == x:               # 如果x是祖先則返回
        return x
    else:
        return find(fa[x])       # 如果不是則x的爸爸問x的爺爺
    
# 帶路徑壓縮
def findEx(x):
    if x != fa[x]:               # x不是自身的父親,即x不是該集合的代表
        fa[x] = find(fa[x])      # 查詢x的祖先直到找到代表,於是順手路徑壓縮
    return fa[x]
    
# 合併
def union(x, y):
    x = find(x)
    y = find(y)
    fa[x] = y                    # 把 x 的祖先變成 y 的祖先的兒子

實現迷宮

思路

  1. 牆和可達點都佔一個地圖格
  2. 初始化地圖為牆和可達點相間隔,任意兩個可達點不連通
  3. 去除規劃路徑上的牆,使之成為一個可達點,從而形成一條路徑
  4. 這裡將障礙牆視為無向圖的邊,因為不需要考慮權重,只需要隨機選擇即可
  5. 執行Kruskal演算法需要確認查詢可達點是否屬於同一個集合,利用並查集優化效率
    參考下圖:黑色塊為牆,白色塊為可達點,路徑(紅色線)上的牆需要去除成為可達點(在Kruskal演算法意為中為選擇的生成樹的邊集)

實現

import random


class Maze():
    def init(self, width, height):
        '''初始化'''
        self.wt = width           # 寬度,初始每行擁有可達點數量
        self.ht = height          # 高度,初始每列擁有可達點數量

        self.fa = {}              # 用於並查集
        self.mp = []              # 地圖
        for i in range(0, height * 2 + 1):
            self.mp.append([1] * (width * 2 + 1))
        for i in range(1, height * 2, 2):
            for j in range(1, width * 2, 2):
                self.mp[i][j] = 0
                self.fa[(i, j)] = (i, j)

    def show(self):
        '''輸出地圖(特殊符號受控制檯字型影響)'''
        for i in range(0, self.ht * 2 + 1, 1):
            print(''.join('■' if j else '  ' for j in self.mp[i]))

    def gen(self):
        '''生成演算法實現'''
        # 統計所有邊(障礙)
        wait = []
        for i in range(1, self.ht * 2, 2):
            for j in range(2, self.wt * 2, 2):
                wait.append((i, j))
        for i in range(2, self.ht * 2, 2):
            for j in range(1, self.wt * 2, 2):
                wait.append((i, j))

        while len(wait):
            idx = random.randint(0, len(wait) - 1)
            e = wait.pop(idx)
            p1, p2 = self.arround(e)
            if self.find(p1) != self.find(p2):
                self.union(p1, p2)
                self.mp[e[0]][e[1]] = 0

    def arround(self, e):
        '''取得障礙兩邊的可達點'''
        if e[0] % 2:
            return (e[0], e[1] - 1), (e[0], e[1] + 1)
        else:
            return (e[0] - 1, e[1]), (e[0] + 1, e[1])

    def find(self, p1):
        '''並查集查詢'''
        if p1 != self.fa[p1]:
            self.fa[p1] = self.find(self.fa[p1])
        return self.fa[p1]

    def union(self, p1, p2):
        '''並查集合並'''
        pp1 = self.find(p1)
        pp2 = self.find(p2)
        self.fa[pp1] = pp2

maze = Maze(19, 19)
maze.gen()
maze.show()

生成效果如下圖:

參考資料:
生成樹 https://oi-wiki.org/graph/mst/
並查集 https://oi-wiki.org/ds/dsu/