三大迷宫生成算法 (Maze generation algorithm) -- 深度优先,随机Prim,递归分割

之前同学参加面试,面试官问到迷宫生成算法,这个问题自己想了下也没有好办法,所以就查询了相关资料。这里进行了相关整理:

本文主要讲解的迷宫生成算法有三种:

1.Recursive backtracker ( 递归回溯,也是深度优先算法)

2.Randomized Prim's algorithm(随机Prim算法,让我想起了最小生成树的Prim算法)

3.Recursive division (递归分割算法)

首先,为了方便后续处理,默认的迷宫元素表示为[x,y,w]

1.我们的迷宫为常规的矩形,因此可以用二维表示一个迷宫单元, 每个迷宫单元表示为一个二维数组元素[x,y]。

3.每个迷宫单元包含左上右下四个属性, 用w表示,分别表示迷宫单元四个面的墙,墙不占据迷宫单元

下面,我们一一介绍这三种算法:


首先是深度优先(递归回溯)算法,这个算法可以表示为(根据维基百科):

1.Make the initial cell the current cell and mark it as visited
2.While there are unvisited cells
	1.If the current cell has any neighbours which have not been visited
		1.Choose randomly one of the unvisited neighbours
		2.Push the current cell to the stack
		3.Remove the wall between the current cell and the chosen cell
		4.Make the chosen cell the current cell and mark it as visited
	2.Else if stack is not empty
		1.Pop a cell from the stack
		2.Make it the current cell
如果翻译一下,就是:

1.将起点作为当前迷宫单元并标记为已访问
2.当还存在未标记的迷宫单元,进行循环
	1.如果当前迷宫单元有未被访问过的的相邻的迷宫单元
		1.随机选择一个未访问的相邻迷宫单元
		2.将当前迷宫单元入栈
		3.移除当前迷宫单元与相邻迷宫单元的墙
		4.标记相邻迷宫单元并用它作为当前迷宫单元
	2.如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
		1.栈顶的迷宫单元出栈
		2.令其成为当前迷宫单元


深度优先构建迷宫的思想就是,每次把新找到的未访问迷宫单元作为优先,寻找其相邻的未访问过的迷宫单元,直到所有的单元都被访问到。通俗的说,就是从起点开始随机走,走不通了就返回上一步,从下一个能走的地方再开始随机走。一般来说,深度优先法生成的迷宫极度扭曲,有着一条明显的主路。我们使用python语言+matplotlib生成的20*30的迷宫如图所示:

三大迷宫生成算法 (Maze generation algorithm) -- 深度优先,随机Prim,递归分割_第1张图片

我们参考维基百科,使用python语言,深度优先迷宫算法如下,代码中都已经加了注释,需要注意的是生成迷宫的算法我们将每个迷宫单元又分成了10*10的着色单元,迷宫单元为8的单位,作色为白色,墙宽2个单元,着色为黑色:

# Code by jollysoul

import random
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.cm as cm

num_rows = int(input("Rows: ")) # number of rows
num_cols = int(input("Columns: ")) # number of columns

# The array M is going to hold the array information for each cell.
# The first four coordinates tell if walls exist on those sides 
# and the fifth indicates if the cell has been visited in the search.
# M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)
M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)

# The array image is going to be the output image to display
image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)

# Set starting row and column
r = 0
c = 0
history = [(r,c)] # The history is the stack of visited locations

# Trace a path though the cells of the maze and open walls along the path.
# We do this with a while loop, repeating the loop until there is no history, 
# which would mean we backtracked to the initial start.
while history: 
    M[r,c,4] = 1 # designate this location as visited
    # check if the adjacent cells are valid for moving to
    check = []
    if c > 0 and M[r,c-1,4] == 0:
        check.append('L')  
    if r > 0 and M[r-1,c,4] == 0:
        check.append('U')
    if c < num_cols-1 and M[r,c+1,4] == 0:
        check.append('R')
    if r < num_rows-1 and M[r+1,c,4] == 0:
        check.append('D')    
    
    if len(check): # If there is a valid cell to move to.
        # Mark the walls between cells as open if we move
        history.append([r,c])
        move_direction = random.choice(check)
        if move_direction == 'L':
            M[r,c,0] = 1
            c = c-1
            M[r,c,2] = 1
        if move_direction == 'U':
            M[r,c,1] = 1
            r = r-1
            M[r,c,3] = 1
        if move_direction == 'R':
            M[r,c,2] = 1
            c = c+1
            M[r,c,0] = 1
        if move_direction == 'D':
            M[r,c,3] = 1
            r = r+1
            M[r,c,1] = 1
    else: # If there are no valid cells to move to.
    # retrace one step back in history if no move is possible
        r,c = history.pop()
    
         
# Open the walls at the start and finish
M[0,0,0] = 1
M[num_rows-1,num_cols-1,2] = 1
    
# Generate the image for display
for row in range(0,num_rows):
    for col in range(0,num_cols):
        cell_data = M[row,col]
        for i in range(10*row+2,10*row+8):
            image[i,range(10*col+2,10*col+8)] = 255
        if cell_data[0] == 1: 
            image[range(10*row+2,10*row+8),10*col] = 255
            image[range(10*row+2,10*row+8),10*col+1] = 255
        if cell_data[1] == 1: 
            image[10*row,range(10*col+2,10*col+8)] = 255
            image[10*row+1,range(10*col+2,10*col+8)] = 255
        if cell_data[2] == 1: 
            image[range(10*row+2,10*row+8),10*col+9] = 255
            image[range(10*row+2,10*row+8),10*col+8] = 255
        if cell_data[3] == 1: 
            image[10*row+9,range(10*col+2,10*col+8)] = 255
            image[10*row+8,range(10*col+2,10*col+8)] = 255
        

# Display the image
plt.imshow(image, cmap = cm.Greys_r, interpolation='none')
plt.show()

然后是随机Prim,随机Prim算法生成的迷宫岔路较多,整体上较为自然而又复杂,算法核心为(根据维基百科)。

1.Start with a grid full of walls.
2.Pick a cell, mark it as part of the maze. Add the walls of the cell to the wall list.
3.While there are walls in the list:
	1.Pick a random wall from the list. If only one of the two cells that the wall divides is visited, then:
		2.Make the wall a passage and mark the unvisited cell as part of the maze.
		3.Add the neighboring walls of the cell to the wall list.
	2.Remove the wall from the list.

1.让迷宫全是墙.
2.选一个单元格作为迷宫的通路,然后把它的邻墙放入列表
3.当列表里还有墙时
	1.从列表里随机选一个墙,如果这面墙分隔的两个单元格只有一个单元格被访问过
		1.那就从列表里移除这面墙,即把墙打通,让未访问的单元格成为迷宫的通路
		2.把这个格子的墙加入列表
	2.如果墙两面的单元格都已经被访问过,那就从列表里移除这面墙
在操作过程中,如果把墙放到列表中,比较复杂,维基里面提到了改进策略:

Although the classical Prim's algorithm keeps a list of edges, for maze generation we could instead maintain a list of adjacent cells. If the randomly chosen cell has multiple edges that connect it to the existing maze, select one of these edges at random. This will tend to branch slightly more than the edge-based version above.

解释一下就是:我们可以维护一个迷宫单元格的列表,而不是边的列表。在这个迷宫单元格列表里面存放了未访问的单元格,我们在单元格列表中随机挑选一个单元格,如果这个单元格有多面墙联系着已存在的迷宫通路,我们就随机选择一面墙打通。这会比基于边的版本分支稍微多一点。

相对于深度优先的算法,Prim随机算法不是优先选择最近选中的单元格,而是随机的从所有的列表中的单元格进行选择,新加入的单元格和旧加入的单元格同样概率会被选择,新加入的单元格没有有优先权。因此其分支更多,生成的迷宫更复杂,难度更大,也更自然。生成的迷宫如图所示:

三大迷宫生成算法 (Maze generation algorithm) -- 深度优先,随机Prim,递归分割_第2张图片

基于随机Prim的迷宫生成算法代码如下:

import random
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.cm as cm

num_rows = int(input("Rows: ")) # number of rows
num_cols = int(input("Columns: ")) # number of columns

# The array M is going to hold the array information for each cell.
# The first four coordinates tell if walls exist on those sides 
# and the fifth indicates if the cell has been visited in the search.
# M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)
M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)

# The array image is going to be the output image to display
image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)

# Set starting row and column
r = 0
c = 0
history = [(r,c)] # The history is the stack of visited locations

# Trace a path though the cells of the maze and open walls along the path.
# We do this with a while loop, repeating the loop until there is no history, 
# which would mean we backtracked to the initial start.
while history: 
	#random choose a candidata cell from the cell set histroy
	r,c = random.choice(history)
	M[r,c,4] = 1 # designate this location as visited
	history.remove((r,c))
	check = []
	# If the randomly chosen cell has multiple edges 
    # that connect it to the existing maze, 
	if c > 0:
		if M[r,c-1,4] == 1:
			check.append('L')
		elif M[r,c-1,4] == 0:
			history.append((r,c-1))
			M[r,c-1,4] = 2
	if r > 0:
		if M[r-1,c,4] == 1: 
			check.append('U') 
		elif M[r-1,c,4] == 0:
			history.append((r-1,c))
			M[r-1,c,4] = 2
	if c < num_cols-1:
		if M[r,c+1,4] == 1: 
			check.append('R')
		elif M[r,c+1,4] == 0:
			history.append((r,c+1))
			M[r,c+1,4] = 2 
	if r < num_rows-1:
		if M[r+1,c,4] == 1: 
			check.append('D') 
		elif  M[r+1,c,4] == 0:
			history.append((r+1,c))
			M[r+1,c,4] = 2

    # select one of these edges at random.
	if len(check):
		move_direction = random.choice(check)
		if move_direction == 'L':
			M[r,c,0] = 1
			c = c-1
			M[r,c,2] = 1
		if move_direction == 'U':
			M[r,c,1] = 1
			r = r-1
			M[r,c,3] = 1
		if move_direction == 'R':
			M[r,c,2] = 1
			c = c+1
			M[r,c,0] = 1
		if move_direction == 'D':
			M[r,c,3] = 1
			r = r+1
			M[r,c,1] = 1
         
# Open the walls at the start and finish
M[0,0,0] = 1
M[num_rows-1,num_cols-1,2] = 1
    
# Generate the image for display
for row in range(0,num_rows):
    for col in range(0,num_cols):
        cell_data = M[row,col]
        for i in range(10*row+2,10*row+8):
            image[i,range(10*col+2,10*col+8)] = 255
        if cell_data[0] == 1: 
            image[range(10*row+2,10*row+8),10*col] = 255
            image[range(10*row+2,10*row+8),10*col+1] = 255
        if cell_data[1] == 1: 
            image[10*row,range(10*col+2,10*col+8)] = 255
            image[10*row+1,range(10*col+2,10*col+8)] = 255
        if cell_data[2] == 1: 
            image[range(10*row+2,10*row+8),10*col+9] = 255
            image[range(10*row+2,10*row+8),10*col+8] = 255
        if cell_data[3] == 1: 
            image[10*row+9,range(10*col+2,10*col+8)] = 255
            image[10*row+8,range(10*col+2,10*col+8)] = 255
        

# Display the image
plt.imshow(image, cmap = cm.Greys_r, interpolation='none')
plt.show()



最后是递归分割,递归分割法生成的迷宫较为简单,有点像四叉树,直路多且不扭曲。通俗的说,就是把空间用十字分成四个子空间,然后在三面墙上挖洞(为了确保连通),之后对每个子空间继续做这件事直到空间不足以继续分割为止。此算法十分高效。

三大迷宫生成算法 (Maze generation algorithm) -- 深度优先,随机Prim,递归分割_第3张图片

源代码如下:

# Code by jollysoul

import random
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.cm as cm

#这个函数将当前区域划分为四个小区域,并随机的在三个区域挖洞,
#让四个区域彼此联通,分隔与挖洞点都是随机生成的。
def Recursive_division(r1, r2, c1, c2, M, image):
	if r1 < r2 and c1 < c2:
		rm = random.randint(r1, r2-1)
		cm = random.randint(c1, c2-1)
		cd1 = random.randint(c1,cm)
		cd2 = random.randint(cm+1,c2)
		rd1 = random.randint(r1,rm)
		rd2 = random.randint(rm+1,r2)
		d = random.randint(1,4)
		if d == 1:
			M[rd2, cm, 2] = 1
			M[rd2, cm+1, 0] = 1
			M[rm, cd1, 3] = 1
			M[rm+1, cd1, 1] = 1
			M[rm, cd2, 3] = 1
			M[rm+1, cd2, 1] = 1
		elif d == 2:
			M[rd1, cm, 2] = 1
			M[rd1, cm+1, 0] = 1
			M[rm, cd1, 3] = 1
			M[rm+1, cd1, 1] = 1
			M[rm, cd2, 3] = 1
			M[rm+1, cd2, 1] = 1
		elif d == 3:
			M[rd1, cm, 2] = 1
			M[rd1, cm+1, 0] = 1
			M[rd2, cm, 2] = 1
			M[rd2, cm+1, 0] = 1
			M[rm, cd2, 3] = 1
			M[rm+1, cd2, 1] = 1
		elif d == 4:
			M[rd1, cm, 2] = 1
			M[rd1, cm+1, 0] = 1
			M[rd2, cm, 2] = 1
			M[rd2, cm+1, 0] = 1
			M[rm, cd1, 3] = 1
			M[rm+1, cd1, 1] = 1

		Recursive_division(r1, rm, c1, cm, M, image)
		Recursive_division(r1, rm, cm+1, c2, M, image)
		Recursive_division(rm+1, r2, cm+1, c2, M, image)
		Recursive_division(rm+1, r2, c1, cm, M, image)

	elif r1 < r2:
		rm = random.randint(r1, r2-1)
		M[rm,c1,3] = 1
		M[rm+1,c1,1] = 1
		Recursive_division(r1, rm, c1, c1, M, image)
		Recursive_division(rm+1, r2, c1, c1, M, image)
	elif c1 < c2:
		cm = random.randint(c1,c2-1)
		M[r1,cm,2] = 1
		M[r1,cm+1,0] = 1
		Recursive_division(r1, r1, c1, cm, M, image)
		Recursive_division(r1, r1, cm+1, c2, M, image)


num_rows = int(input("Rows: ")) # number of rows
num_cols = int(input("Columns: ")) # number of columns
r1 = 0
r2 = num_rows-1
c1 = 0
c2 = num_cols-1

# The array M is going to hold the array information for each cell.
# The first four coordinates tell if walls exist on those sides 
# and the fifth indicates if the cell has been visited in the search.
# M(LEFT, UP, RIGHT, DOWN, CHECK_IF_VISITED)
M = np.zeros((num_rows,num_cols,5), dtype=np.uint8)

# The array image is going to be the output image to display
image = np.zeros((num_rows*10,num_cols*10), dtype=np.uint8)
 
Recursive_division(r1, r2, c1, c2, M, image)  

# Open the walls at the start and finish
M[0,0,0] = 1
M[num_rows-1,num_cols-1,2] = 1
    
# Generate the image for display
for row in range(0,num_rows):
    for col in range(0,num_cols):
        cell_data = M[row,col]
        for i in range(10*row+2,10*row+8):
            image[i,range(10*col+2,10*col+8)] = 255
        if cell_data[0] == 1: 
            image[range(10*row+2,10*row+8),10*col] = 255
            image[range(10*row+2,10*row+8),10*col+1] = 255
        if cell_data[1] == 1: 
            image[10*row,range(10*col+2,10*col+8)] = 255
            image[10*row+1,range(10*col+2,10*col+8)] = 255
        if cell_data[2] == 1: 
            image[range(10*row+2,10*row+8),10*col+9] = 255
            image[range(10*row+2,10*row+8),10*col+8] = 255
        if cell_data[3] == 1: 
            image[10*row+9,range(10*col+2,10*col+8)] = 255
            image[10*row+8,range(10*col+2,10*col+8)] = 255
        

# Display the image
plt.imshow(image, cmap = cm.Greys_r, interpolation='none')
plt.show()


总结来说,这三种算法分别适合不同的迷宫情况,深度优先适合于那种主线支线明显的游戏(如RPG),而递归分割则适合转角较少的游戏(也许是FPS和ACT),至于prim,似乎适合最标准的迷宫游戏(因为很难走)。

基本就这些了……


部分参考自:维基百科 以及 http://bbs.9ria.com/thread-156150-1-1.html









你可能感兴趣的:(算法,迷宫生成,prim,算法)