Python 四大迷宫生成算法实现(4): 生成树+并查集算法

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/marble_xu/article/details/89329862

生成树算法简介

先看下生成树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()

猜你喜欢

转载自blog.csdn.net/marble_xu/article/details/89329862
今日推荐