先看下生成树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。
在我们的代码实现中,比如迷宫地图的高度为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) 表示迷宫单元的位置,第一个表示树的根节点。
这个和之前的递归回溯算法使用相同的地图类,这里就省略了。
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()