遞歸回溯算法簡介
下圖是算法使用的地圖,地圖最外圍默認是一圈牆,其中白色單元是迷宮單元,黑色單元是牆,相鄰白色單元之前的牆是可以被去掉的。可以看到這個地圖中所有的迷宮單元在地圖中的位置(X,Y),比如(1,1),(5,9)都是奇數,可以表示成(2 * x+1, 2 * y+1), x和y的取值範圍從0到4。在迷宮生成算法中會用到這個表示方式。同時迷宮的長度和寬度必須爲奇數。
遞歸回溯是一個深度優先算法,如果當前單元有相鄰的未訪問過的迷宮單元,就一直向前搜索,直到當前單元沒有未訪問過的迷宮單元,才返回查找之前搜索路徑上未訪問的迷宮單元,所以用堆棧來維護已訪問過的迷宮單位。
算法主循環,重複下面步驟2直到堆棧爲空:
1 隨機選擇一個迷宮單元作爲起點,加入堆棧並標記爲已訪問
2 當堆棧非空時,從棧頂獲取一個迷宮單元(不用出棧),進行循環
- 如果當前迷宮單元有未被訪問過的相鄰迷宮單元
- 隨機選擇一個未訪問的相鄰迷宮單元
- 去掉當前迷宮單元與相鄰迷宮單元之間的牆
- 標記相鄰迷宮單元爲已訪問,並將它加入堆棧
- 否則,當前迷宮單元沒有未訪問的相鄰迷宮單元
- 則棧頂的迷宮單元出棧
關鍵代碼介紹
保存基本信息的地圖類
地圖類用於保存和獲取迷宮算法使用的基礎地圖信息。
- 先創建一個map類, 初始化參數設置地圖的長度和寬度,並設置保存地圖信息的二維數據map的值爲0, 值爲0表示該單元可以移動,值爲1表示該單元是牆。定義在算法中用到的Enum 類MAP_ENTRY_TYPE 和 WALL_DIRECTION。
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)]
- 在map類中添加將整個map單元設置爲某個值的函數,設置某個單元爲某個值的函數,和某個單元是否被訪問的函數。
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 isVisited(self, x, y):
return self.map[y][x] != 1
- 在map類中添加一個顯示地圖的函數,可以看到,這邊只是簡單的打印出所有節點的值,值爲1時打印’#"表示強,或1的意思上面已經說明,在後面顯示尋路算法結果時,會使用到值2,表示一條從開始節點到目標節點的路徑。
def showMap(self):
for row in self.map:
s = ''
for entry in row:
if entry == 0:
s += ' 0'
elif entry == 1:
s += ' #'
else:
s += ' X'
print(s)
算法主函數介紹
doRecursiveBacktracker 函數 先調用resetMap函數將地圖都設置爲牆,迷宮單元所在位置爲牆表示未訪問。有個注意點是地圖的長寬和迷宮單元的位置取值範圍的對應關係。
假如地圖的寬度是31,長度是21,對應的迷宮單元的位置取值範圍是 x(0,15), y(0,10), 因爲迷宮單元(x,y)對應到地圖上的位置是(2 * x+1, 2 * y+1)。
recursiveBacktracker 函數就是上面算法主循環的實現。
# recursive backtracker algorithm
def recursiveBacktracker(map, width, height):
startX, startY = (randint(0, width-1), randint(0, height-1))
print("start(%d, %d)" % (startX, startY))
map.setMap(2*startX+1, 2*startY+1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist = []
checklist.append((startX, startY))
while len(checklist):
# use checklist as a stack, get entry from the top of stack
entry = checklist[-1]
if not checkAdjacentPos(map, entry[0], entry[1], width, height, checklist):
# the entry has no unvisited adjacent entry, so remove it from checklist
checklist.remove(entry)
def doRecursiveBacktracker(map):
# set all entries of map to wall
map.resetMap(MAP_ENTRY_TYPE.MAP_BLOCK)
recursiveBacktracker(map, (map.width-1)//2, (map.height-1)//2)
checkAdjacentPos 函數檢查當前迷宮單元的是否有未訪問的相鄰單元,如果有,則隨即選取一個相鄰單元,標記未已訪問,並去掉當前迷宮單元與相鄰迷宮單元之間的牆。如果沒有,則不做操作。
# find unvisited adjacent entries of four possible entris
# then add random one of them to checklist and mark it as visited
def checkAdjacentPos(map, x, y, width, height, checklist):
directions = []
if x > 0:
if not map.isVisited(2*(x-1)+1, 2*y+1):
directions.append(WALL_DIRECTION.WALL_LEFT)
if y > 0:
if not map.isVisited(2*x+1, 2*(y-1)+1):
directions.append(WALL_DIRECTION.WALL_UP)
if x < width -1:
if not map.isVisited(2*(x+1)+1, 2*y+1):
directions.append(WALL_DIRECTION.WALL_RIGHT)
if y < height -1:
if not map.isVisited(2*x+1, 2*(y+1)+1):
directions.append(WALL_DIRECTION.WALL_DOWN)
if len(directions):
direction = choice(directions)
if direction == WALL_DIRECTION.WALL_LEFT:
map.setMap(2*(x-1)+1, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x-1, y))
elif direction == WALL_DIRECTION.WALL_UP:
map.setMap(2*x+1, 2*(y-1)+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x+1, 2*y, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x, y-1))
elif direction == WALL_DIRECTION.WALL_RIGHT:
map.setMap(2*(x+1)+1, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x+2, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x+1, y))
elif direction == WALL_DIRECTION.WALL_DOWN:
map.setMap(2*x+1, 2*(y+1)+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x+1, 2*y+2, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x, y+1))
return True
else:
# if not find any unvisited adjacent entry
return False
代碼的初始化
可以調整地圖的長度,寬度,注意長度和寬度必須爲奇數。
def run():
WIDTH = 31
HEIGHT = 21
map = Map(WIDTH, HEIGHT)
doRecursiveBacktracker(map)
map.showMap()
if __name__ == "__main__":
run()
執行的效果圖如下,start 表示第一個隨機選擇的迷宮單元。迷宮中’#'表示牆,'0’表示通道。
完整代碼
使用python3.7編譯
from random import randint, 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 isVisited(self, x, y):
return self.map[y][x] != 1
def showMap(self):
for row in self.map:
s = ''
for entry in row:
if entry == 0:
s += ' 0'
elif entry == 1:
s += ' #'
else:
s += ' X'
print(s)
# find unvisited adjacent entries of four possible entris
# then add random one of them to checklist and mark it as visited
def checkAdjacentPos(map, x, y, width, height, checklist):
directions = []
if x > 0:
if not map.isVisited(2*(x-1)+1, 2*y+1):
directions.append(WALL_DIRECTION.WALL_LEFT)
if y > 0:
if not map.isVisited(2*x+1, 2*(y-1)+1):
directions.append(WALL_DIRECTION.WALL_UP)
if x < width -1:
if not map.isVisited(2*(x+1)+1, 2*y+1):
directions.append(WALL_DIRECTION.WALL_RIGHT)
if y < height -1:
if not map.isVisited(2*x+1, 2*(y+1)+1):
directions.append(WALL_DIRECTION.WALL_DOWN)
if len(directions):
direction = choice(directions)
#print("(%d, %d) => %s" % (x, y, str(direction)))
if direction == WALL_DIRECTION.WALL_LEFT:
map.setMap(2*(x-1)+1, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x-1, y))
elif direction == WALL_DIRECTION.WALL_UP:
map.setMap(2*x+1, 2*(y-1)+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x+1, 2*y, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x, y-1))
elif direction == WALL_DIRECTION.WALL_RIGHT:
map.setMap(2*(x+1)+1, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x+2, 2*y+1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x+1, y))
elif direction == WALL_DIRECTION.WALL_DOWN:
map.setMap(2*x+1, 2*(y+1)+1, MAP_ENTRY_TYPE.MAP_EMPTY)
map.setMap(2*x+1, 2*y+2, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist.append((x, y+1))
return True
else:
# if not find any unvisited adjacent entry
return False
# recursive backtracker algorithm
def recursiveBacktracker(map, width, height):
startX, startY = (randint(0, width-1), randint(0, height-1))
print("start(%d, %d)" % (startX, startY))
map.setMap(2*startX+1, 2*startY+1, MAP_ENTRY_TYPE.MAP_EMPTY)
checklist = []
checklist.append((startX, startY))
while len(checklist):
# use checklist as a stack, get entry from the top of stack
entry = checklist[-1]
if not checkAdjacentPos(map, entry[0], entry[1], width, height, checklist):
# the entry has no unvisited adjacent entry, so remove it from checklist
checklist.remove(entry)
def doRecursiveBacktracker(map):
# set all entries of map to wall
map.resetMap(MAP_ENTRY_TYPE.MAP_BLOCK)
recursiveBacktracker(map, (map.width-1)//2, (map.height-1)//2)
def run():
WIDTH = 31
HEIGHT = 21
map = Map(WIDTH, HEIGHT)
doRecursiveBacktracker(map)
map.showMap()
if __name__ == "__main__":
run()