Python 四大迷宮生成算法實現(4): 生成樹+並查集算法

生成樹算法簡介

先看下生成樹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()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章