生成樹算法簡介
先看下生成樹Kruskal算法:
1 一開始將每個點作爲單獨的一棵樹,選擇一個起點和終點。
2 循環執行,隨機選擇一條邊,判斷邊連接的頂點,是否在同一子樹中。
- 如果不是,則連通這兩個頂點,把他們任意一個添加到另一個所在的子樹中。
- 如果是,則判斷起點和終點是否在同一子樹中。如果在同一子樹中,表示已經生成了一顆樹,則退出循環。
上面生成樹算法中的邊可以看成是迷宮中的牆,但是在實現時要同時記錄牆和迷宮單元的信息,會比較複雜,所以使用改進版本,只維護一個迷宮單元的列表,判斷迷宮單元和它相鄰的迷宮單元是否在同一棵樹中。
合併兩個頂點的時候,想要實現高效率的話,使用線性表肯定是不行的,所以這裏需要使用UFS(Union_Find_Set)並查集。
下圖是算法使用的地圖,地圖最外圍默認是一圈牆,其中白色單元是迷宮單元,黑色單元是牆,相鄰白色單元之前的牆是可以被去掉的。可以看到這個地圖中所有的迷宮單元在地圖中的位置(X,Y),比如(1,1),(5,9)都是奇數,可以表示成(2 * x+1, 2 * y+1), x和y的取值範圍從0到4。在迷宮生成算法中會用到這個表示方式。同時迷宮的長度和寬度必須爲奇數。
算法主循環,重複下面步驟2直到檢查列表爲空:
1 將每個迷宮單元都初始化爲單獨的一棵樹,並加入檢查列表。
2 當檢查列表非空時,隨機從列表中取出一個迷宮單元,檢查當前迷宮單元和它的相鄰迷宮單元,是否屬於同一棵樹。
- 如果有相鄰迷宮單元不屬於同一棵樹,隨機選擇一個這樣的相鄰迷宮單元
- 使用並查集方法將這個相鄰迷宮單元和當前迷宮單元合併成一顆樹
- 否則,表示當前迷宮單元和相鄰的迷宮單元都屬於同一棵樹
- 則從檢查列表刪除當前迷宮單元
並查集算法簡介
並查集存儲
每棵樹都有一個唯一的根節點,可以用它來代表這個樹所有節點所在的Set。
- 初始化:每個節點(x, y)看做一棵樹,當然這是一棵只有根節點的樹,將這個節點的值作爲set的標識。
- 查詢:對於節點(x,y),通過節點的值不斷查找它的父節點,直到找到所在樹的根節點;
在我們的代碼實現中,比如迷宮地圖的高度爲10,則一個迷宮單元(x,y), 它的節點值爲: index = x*height+y。parentlist[index] 表示它的父節點值。初始化時,parentlist[index] = index,表示節點的父節點是它自己。
parentlist = [x*height+y for x in range(width) for y in range(height)]
判斷兩個節點是否屬於同一棵樹
這個很簡單,找到兩個節點(x1,y1) 和 (x2, y2) 所在樹的根節點,判斷兩個根節點是否相同。
並查集合並規則
在每棵樹的根節點存儲一個屬性 weight,用來表示這棵樹擁有的子節點數,節點數多的是“大樹”,少的就是“小樹”。有一個合併兩棵子樹的原則是小樹變成大樹的子樹,這樣生成的樹更加平衡。
比如下面的兩顆樹,(x,y) 表示迷宮單元的位置,第一個表示樹的根節點。
- 合併前
(2,2) <= (3,2)
(0,3) <= (2,4) <= (3,3) - 小樹變成大樹的子樹合併後,根節點爲(0, 3),樹高度爲3
<= (2,2) <= (3,2)
(0,3)
<= (2,4) <= (3,3) - 如果大樹變成小樹的子樹合併後,根節點爲(2, 2),樹高度爲4
<= (3,2)
(2,2)
<= (0,3) <= (2,4) <= (3,3)
關鍵代碼介紹
保存基本信息的地圖類
這個和之前的遞歸回溯算法使用相同的地圖類,這裏就省略了。
算法主函數介紹
doUnionFindSet 函數 先調用resetMap函數將地圖都設置爲牆。有個注意點是地圖的長寬和迷宮單元的位置取值範圍的對應關係。
假如地圖的寬度是31,長度是21,對應的迷宮單元的位置取值範圍是 x(0,15), y(0,10), 因爲迷宮單元(x,y)對應到地圖上的位置是(2 * x+1, 2 * y+1)。
unionFindSet 函數就是上面算法主循環的實現。這邊會先做初始化,將地圖中的迷宮單元設爲空,添加所有迷宮單元到 checklist 檢查列表。
parentlist 表示每個迷宮單元的父節點,初始化爲迷宮單元自己,即單獨的一棵樹。
weightlist 表示每個迷宮單元的權重,初始化爲1,在合併子樹時使用,保證生成樹的平衡,防止生成樹的高度過大。
def unionFindSet(map, width, height):
parentlist = [x*height+y for x in range(width) for y in range(height)]
weightlist = [1 for x in range(width) for y in range(height)]
checklist = []
for x in range(width):
for y in range(height):
checklist.append((x,y))
# set all entries to empty
map.setMap(2*x+1, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
while len(checklist):
# select a random entry from checklist
entry = choice(checklist)
if not checkAdjacentPos(map, entry[0], entry[1], width, height, parentlist, weightlist):
checklist.remove(entry)
def doUnionFindSet(map):
# set all entries of map to wall
map.resetMap(MAP_ENTRY_TYPE.MAP_BLOCK)
unionFindSet(map, (map.width-1)//2, (map.height-1)//2)
checkAdjacentPos 函數 檢查當前迷宮單元和它的相鄰迷宮單元,是否屬於同一棵樹。如果存在不屬於同一棵樹的相鄰迷宮單元列表,則從中選取一個,打通當前迷宮單元和這個相鄰迷宮單元之間的牆,並合併成一顆樹。
def checkAdjacentPos(map, x, y, width, height, parentlist, weightlist):
directions = []
node1 = getNodeIndex(x,y)
root1 = findSet(parentlist, node1)
# check four adjacent entries, add any unconnected entries
if x > 0:
root2 = findSet(parentlist, getNodeIndex(x-1, y))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_LEFT)
if y > 0:
root2 = findSet(parentlist, getNodeIndex(x, y-1))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_UP)
if x < width -1:
root2 = findSet(parentlist, getNodeIndex(x+1, y))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_RIGHT)
if y < height -1:
root2 = findSet(parentlist, getNodeIndex(x, y+1))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_DOWN)
if len(directions):
# choose one of the unconnected adjacent entries
direction = choice(directions)
if direction == WALL_DIRECTION.WALL_LEFT:
adj_x, adj_y = (x-1, y)
map.setMap(2*x, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
elif direction == WALL_DIRECTION.WALL_UP:
adj_x, adj_y = (x, y-1)
map.setMap(2*x+1, 2*y, MAP_ENTRY_TYPE.MAP_EMPTY)
elif direction == WALL_DIRECTION.WALL_RIGHT:
adj_x, adj_y = (x+1, y)
map.setMap(2*x+2, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
elif direction == WALL_DIRECTION.WALL_DOWN:
adj_x, adj_y = (x, y+1)
map.setMap(2*x+1, 2*y+2, MAP_ENTRY_TYPE.MAP_EMPTY)
node2 = getNodeIndex(adj_x, adj_y)
unionSet(parentlist, node1, node2, weightlist)
return True
else:
# the four adjacent entries are all connected, so can remove this entry
return False
findSet 函數返回迷宮單元所在樹的根節點。
getNodeIndex 函數返回迷宮單元 (x, y) 的樹節點index。
unionSet 函數進行合併兩顆樹的操作
# find the root of the tree which the node belongs to
def findSet(parent, index):
if index != parent[index]:
return findSet(parent, parent[index])
return parent[index]
def getNodeIndex(x, y):
return x * height + y
# union two unconnected trees
def unionSet(parent, index1, index2, weightlist):
root1 = findSet(parent, index1)
root2 = findSet(parent, index2)
if root1 == root2:
return
if root1 != root2:
# take the high weight tree as the root,
# make the whole tree balance to achieve everage search time O(logN)
if weightlist[root1] > weightlist[root2]:
parent[root2] = root1
weightlist[root1] += weightlist[root2]
else:
parent[root1] = root2
weightlist[root2] += weightlist[root1]
代碼的初始化
可以調整地圖的長度,寬度,注意長度和寬度必須爲奇數。
def run():
WIDTH = 31
HEIGHT = 21
map = Map(WIDTH, HEIGHT)
doRecursiveBacktracker(map)
map.showMap()
if __name__ == "__main__":
run()
執行的效果圖如下,start 表示第一個隨機選擇的迷宮單元。迷宮中’#‘表示牆,空格’ '表示通道。
完整代碼
使用python3.7編譯,有一個debug 函數printTree,可以打印出最後生成的樹結構。
from random import choice
from enum import Enum
class MAP_ENTRY_TYPE(Enum):
MAP_EMPTY = 0,
MAP_BLOCK = 1,
class WALL_DIRECTION(Enum):
WALL_LEFT = 0,
WALL_UP = 1,
WALL_RIGHT = 2,
WALL_DOWN = 3,
class Map():
def __init__(self, width, height):
self.width = width
self.height = height
self.map = [[0 for x in range(self.width)] for y in range(self.height)]
def resetMap(self, value):
for y in range(self.height):
for x in range(self.width):
self.setMap(x, y, value)
def setMap(self, x, y, value):
if value == MAP_ENTRY_TYPE.MAP_EMPTY:
self.map[y][x] = 0
elif value == MAP_ENTRY_TYPE.MAP_BLOCK:
self.map[y][x] = 1
def showMap(self):
for row in self.map:
s = ''
for entry in row:
if entry == 0:
s += ' '
elif entry == 1:
s += ' #'
else:
s += ' X'
print(s)
def unionFindSet(map, width, height):
# find the root of the tree which the node belongs to
def findSet(parent, index):
if index != parent[index]:
return findSet(parent, parent[index])
return parent[index]
def getNodeIndex(x, y):
return x * height + y
# union two unconnected trees
def unionSet(parent, index1, index2, weightlist):
root1 = findSet(parent, index1)
root2 = findSet(parent, index2)
if root1 == root2:
return
if root1 != root2:
# take the high weight tree as the root,
# make the whole tree balance to achieve everage search time O(logN)
if weightlist[root1] > weightlist[root2]:
parent[root2] = root1
weightlist[root1] += weightlist[root2]
else:
parent[root1] = root2
weightlist[root2] += weightlist[root2]
# For Debug: print the generate tree
def printPath(parent, x, y):
node = x * height + y
path = '(' + str(node//height) +','+ str(node%height)+')'
node = parent[node]
while node != parent[node]:
path = '(' + str(node//height) +','+ str(node%height)+') <= ' + path
node = parent[node]
path = '(' + str(node//height) +','+ str(node%height)+') <= ' + path
print(path)
def printTree(parent):
for x in range(width):
for y in range(height):
printPath(parentlist, x, y)
def checkAdjacentPos(map, x, y, width, height, parentlist, weightlist):
directions = []
node1 = getNodeIndex(x,y)
root1 = findSet(parentlist, node1)
# check four adjacent entries, add any unconnected entries
if x > 0:
root2 = findSet(parentlist, getNodeIndex(x-1, y))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_LEFT)
if y > 0:
root2 = findSet(parentlist, getNodeIndex(x, y-1))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_UP)
if x < width -1:
root2 = findSet(parentlist, getNodeIndex(x+1, y))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_RIGHT)
if y < height -1:
root2 = findSet(parentlist, getNodeIndex(x, y+1))
if root1 != root2:
directions.append(WALL_DIRECTION.WALL_DOWN)
if len(directions):
# choose one of the unconnected adjacent entries
direction = choice(directions)
if direction == WALL_DIRECTION.WALL_LEFT:
adj_x, adj_y = (x-1, y)
map.setMap(2*x, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
elif direction == WALL_DIRECTION.WALL_UP:
adj_x, adj_y = (x, y-1)
map.setMap(2*x+1, 2*y, MAP_ENTRY_TYPE.MAP_EMPTY)
elif direction == WALL_DIRECTION.WALL_RIGHT:
adj_x, adj_y = (x+1, y)
map.setMap(2*x+2, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
elif direction == WALL_DIRECTION.WALL_DOWN:
adj_x, adj_y = (x, y+1)
map.setMap(2*x+1, 2*y+2, MAP_ENTRY_TYPE.MAP_EMPTY)
node2 = getNodeIndex(adj_x, adj_y)
unionSet(parentlist, node1, node2, weightlist)
return True
else:
# the four adjacent entries are all connected, so can remove this entry
return False
parentlist = [x*height+y for x in range(width) for y in range(height)]
weightlist = [1 for x in range(width) for y in range(height)]
checklist = []
for x in range(width):
for y in range(height):
checklist.append((x,y))
# set all entries to empty
map.setMap(2*x+1, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
while len(checklist):
# select a random entry from checklist
entry = choice(checklist)
if not checkAdjacentPos(map, entry[0], entry[1], width, height, parentlist, weightlist):
checklist.remove(entry)
#printTree(parentlist)
def doUnionFindSet(map):
# set all entries of map to wall
map.resetMap(MAP_ENTRY_TYPE.MAP_BLOCK)
unionFindSet(map, (map.width-1)//2, (map.height-1)//2)
def run():
WIDTH = 31
HEIGHT = 21
map = Map(WIDTH, HEIGHT)
doUnionFindSet(map)
map.showMap()
if __name__ == "__main__":
run()