从 0 到 1 理解 DFS 迷宫生成代码:一篇由浅入深的解析
作者:高玉涵
时间:2025.09.09 11:10
博客:blog.csdn.net/cg_i
语言:Python
引子
如果你曾好奇游戏或解谜场景中的迷宫是如何生成的,那么这篇文章将带你通过一段 Python 代码,揭开基于深度优先搜索(DFS)的迷宫生成原理。我们会从代码的整体功能入手,逐步拆解每个模块的作用,最终让你不仅能看懂代码,还能理解其背后的算法逻辑。
一、先睹为快:代码到底做了什么?
在深入代码前,我们先看最终效果(图 1-1):运行代码后,会生成一个 60 列、40 行的随机迷宫,左上角有绿色的 “START” 标记(起点),右下角有红色的 “END” 标记(出口)。这个迷宫没有回路,只有一条唯一路径能从起点走到出口,这就是游戏中常见的 “完美迷宫”。
要实现这个效果,我们需要两个核心工具:
-
matplotlib 库:负责在电脑屏幕上绘制迷宫的墙壁、文字和标记;
-
DFS 算法(深度优先搜索):负责随机 “打通” 墙壁,生成无回路的迷宫路径。

二、基础准备:看懂开头的 “铺垫” 代码
任何程序都需要先做 “准备工作”,这份代码的开头主要是导入工具、设定迷宫大小,这些是后续所有功能的基础。
1.导入必要的库
import sys
import matplotlib.pyplot as plt
from random import randint
-
sys 库:用来调整 Python 的 “递归深度”(后面会解释为什么需要);
-
matplotlib.pyplot:简称 “plt”,是画图的核心工具 —— 迷宫的墙壁、“START” 文字都靠它绘制;
-
random.randint:用来生成随机数,让每次生成的迷宫路径都不一样。
2.设定迷宫大小和递归限制
WIDTH = 60 # 迷宫宽度(一共60列小方格)
HEIGHT = 40 # 迷宫高度(一共40行小方格)
sys.setrecursionlimit(WIDTH * HEIGHT)
-
WIDTH 和 HEIGHT:决定迷宫的大小,数字越大,迷宫越复杂(60x40 的迷宫有 2400 个小方格);
-
递归限制:DFS 算法会用到 “递归”(函数自己调用自己),Python 默认的递归次数有限(1000次),这里把限制设为 “60*40=2400”,避免程序报错。
三、核心工具函数:迷宫的 “基础零件”
就像盖房子需要砖和水泥,生成迷宫也需要几个 “基础工具函数”,负责记录状态、绘制墙壁等基础操作。我们逐个拆解,每个函数都搭配实例说明。
1.记录单元格是否被访问:initVisitedList ()
迷宫是由一个个 “小方格”(称为 “单元格”)组成的(图 3-1),生成迷宫时需要知道哪些单元格已经 “探索过”。这个函数就是创建一个 “访问记录表”:

def initVisitedList():
"""初始化访问记录列表(行优先:visited[y][x])"""
visited = []
for y in range(HEIGHT): # 先遍历每一行(y从0到39)
line = [False] * WIDTH # 每一行有60个False(未访问)
visited.append(line)
return visited
-
实例理解:生成的
visited是一个 40 行、60 列的 “表格”,visited[0][0]代表 “第 0 行第 0 列” 的单元格(左下角),初始值为False(未访问);当我们探索过这个单元格后,会把它改成True。 -
关键提醒:这里是 “行优先” 存储,即
visited[y][x]—— 先写行号y,再写列号x,不要搞反!
2.绘制和删除墙壁:drawLine () & removeLine ()
迷宫的 “墙壁” 就是一条一条的直线,这两个函数分别负责 “画墙” 和 “拆墙”:
def drawLine(x1, y1, x2, y2):
"""绘制墙壁(黑色线条,加粗2px)"""
plt.plot([x1, x2], [y1, y2], color="black", linewidth=2)
def removeLine(x1, y1, x2, y2):
"""删除墙壁(用白色线条覆盖)"""
plt.plot([x1, x2], [y1, y2], color="white")
- 实例理解:调用
drawLine(0,0,0,1)会画一条从(0,0)到(0,1)的黑色竖线,这就是 “第 0 列第 0 行” 单元格的左边墙壁;如果调用removeLine(0,0,0,1),会用白色覆盖这条线,看起来就像 “墙壁被拆了”(打通了路径)。
小细节:在
matplotlib中,默认的坐标系是:原点在左下角,x轴向右为正方向,y轴向上为正方向。
3.获取单元格的 4 条边:get_edges ()
每个单元格都有 4 条边(上、下、左、右),这个函数用来计算每条边的两个端点坐标:
def get_edges(x, y):
"""获取单元格(x,y)的4条边(x=列,y=行)"""
return [
(x, y, x, y+1), # 左边:竖线(x固定,y从y到y+1)
(x+1, y, x+1, y+1), # 右边:竖线(x+1固定)
(x, y, x+1, y), # 下边:横线(y固定)
(x, y+1, x+1, y+1) # 上边:横线(y+1固定)
]
-
核心前提:单元格(x,y)占据的区域是 “从 x 到 x+1、y 到 y+1” 的正方形(例如单元格(0,0)占据 x=0~1 、y=0~1 的区域)。
-
实例理解:以单元格(0,0)为例,调用
get_edges(0,0)会返回 4 条边的坐标:-
左边:(0,0,0,1)——x=0 固定,y 从 0 到 1 的竖线;
-
右边:(1,0,1,1)——x=1 固定,y 从 0 到 1 的竖线;
-
下边:(0,0,1,0)——y=0 固定,x 从 0 到 1 的横线;
-
上边:(0,1,1,1)——y=1 固定,x 从 0 到 1 的横线。
-
4.找两个单元格的公共边:getCommonEdge ()
要 “打通” 两个相邻的单元格,首先要找到它们之间的 “公共边”(也就是共用的那面墙)。这个函数就是干这个的:
def getCommonEdge(cell1_x, cell1_y, cell2_x, cell2_y):
"""获取两个相邻单元格的公共边"""
edges1 = set(get_edges(cell1_x, cell1_y)) # 第一个单元格的边(转成集合)
edges2 = set(get_edges(cell2_x, cell2_y)) # 第二个单元格的边(转成集合)
common = edges1 & edges2 # 集合交集:找出两边都有的边(公共边)
return common.pop() if common else None
- 实例理解:单元格(0,0)和(1,0)是左右相邻,它们的公共边是(1,0,1,1)—— 也就是(0,0)的右边、(1,0)的左边。找到这条边后,删除它就能打通两个单元格(图 3-2)。
- 为什么用集合? 集合的 “交集” 操作能快速找到共同元素,比遍历列表效率更高。

5.初始化所有墙壁:initEdgeList ()
生成迷宫前,我们需要先画好 “完整的网格”(所有单元格的所有边),之后再通过 “拆墙” 形成迷宫。这个函数用来创建所有边的集合:
def initEdgeList():
"""初始化所有边的集合(去重)"""
edges = set()
for x in range(WIDTH):
for y in range(HEIGHT):
edges.update(get_edges(x, y)) # 把每个单元格的边添加到集合
return edges
- 关键作用:用set存储边能自动去重。例如(0,0)的右边和(1,0)的左边是同一条边,添加到集合后只会保留一次,避免重复画墙
6.验证单元格是否有效:isValidPosition ()
确保我们不会探索迷宫之外的单元格(比如 x=-1 或 x=60,这些都是无效位置):
def isValidPosition(x, y):
"""验证单元格(x,y)是否在迷宫范围内"""
return 0 <= x < WIDTH and 0 <= y < HEIGHT
- 实例理解:
isValidPosition(59,39)返回True(60 列的 x 最大是 59,40 行的 y 最大是 39);isValidPosition(60,0)返回False(x=60 超出了宽度)。
7.随机打乱方向:shuffle ()
为了让迷宫路径更 “随机”,每次探索前都要打乱 4 个方向(上、下、左、右)的顺序:
def shuffle(dX, dY):
"""随机打乱4个方向的顺序"""
for _ in range(4):
i, j = randint(0, 3), randint(0, 3)
dX[i], dX[j] = dX[j], dX[i]
dY[i], dY[j] = dY[j], dY[i]
- 作用:如果不打乱方向,每次都会按 “上→下→左→右” 的固定顺序探索,生成的迷宫会很规律、不好玩;打乱后,每次的探索路径都不同,迷宫更随机。
四、迷宫生成的 “大脑”:DFS 核心函数详解
DFS(深度优先搜索)是生成迷宫的核心,它的思路就像 “蒙眼走迷宫”:从起点出发,随机选一个没去过的方向走,走不通就回头换方向,直到所有单元格都走过。
1.DFS 函数完整代码
def DFS(X, Y, edgeList, visited):
"""DFS核心函数:从单元格(X,Y)开始探索"""
# 方向偏移量:上(y+1)、下(y-1)、左(x-1)、右(x+1)
dX = [0, 0, -1, 1]
dY = [1, -1, 0, 0]
shuffle(dX, dY) # 随机打乱方向
for i in range(4):
nextX = X + dX[i] # 计算下一个单元格的x坐标
nextY = Y + dY[i] # 计算下一个单元格的y坐标
# 验证下一个单元格有效且未访问
if isValidPosition(nextX, nextY) and not visited[nextY][nextX]:
visited[nextY][nextX] = True # 标记为已访问
# 找到并删除公共边(打通墙壁)
common_edge = getCommonEdge(X, Y, nextX, nextY)
if common_edge in edgeList:
edgeList.remove(common_edge)
# 递归调用自己,以上一个单元格为起点继续探索
DFS(nextX, nextY, edgeList, visited)
2.逐行拆解 DFS 逻辑(结合实例)
我们以 “起点(0,39)”(左上角,x=0,y=39)为例,一步步看 DFS 是如何工作的:
步骤 1:定义方向偏移量
dX = [0, 0, -1, 1]
dY = [1, -1, 0, 0]
- 偏移量含义:每个索引对应一个方向,通过 “当前坐标 + 偏移量” 得到下一个单元格的坐标:
| 索引 | dX | dY | 方向 |
|---|---|---|---|
| 0 | 0 | 1 | 向上 |
| 1 | 0 | -1 | 向下 |
| 2 | -1 | 0 | 向左 |
| 3 | 1 | 0 | 向右 |
- 实例:当前在(0,39),若选择 “向右”(索引 3),则 nextX=0+1=1,nextY=39+0=39,即下一个单元格是(1,39)。
步骤 2:打乱方向顺序
shuffle(dX, dY)
- 假设打乱后 dX 变成 [1,0,-1,0],dY 变成 [0,1,0,-1],则方向顺序变成 “右→上→左→下”—— 每次运行顺序都不同,保证迷宫随机性。
步骤 3:遍历每个方向,尝试探索
for i in range(4):
nextX = X + dX[i]
nextY = Y + dY[i]
- 循环遍历 4 个方向,计算每个方向的下一个单元格坐标。
步骤 4:验证下一个单元格是否可探索
if isValidPosition(nextX, nextY) and not visited[nextY][nextX]:
-
两个条件都满足才能探索:
a.
isValidPosition(nextX, nextY):下一个单元格在迷宫范围内;b.
not visited[nextY][nextX]:下一个单元格未被访问过。 -
实例:若下一个单元格是(1,39),验证结果为
True(在范围内且未访问),则继续;若下一个单元格是(-1,39),验证失败,跳过该方向。
步骤 5:标记为已访问并打通墙壁
visited[nextY][nextX] = True # 标记下一个单元格为已访问
common_edge = getCommonEdge(X, Y, nextX, nextY) # 找公共边
if common_edge in edgeList:
edgeList.remove(common_edge) # 删除公共边(打通墙壁)
- 实例:标记(1,39)为
True,找到(0,39)和(1,39)的公共边(1,39,1,40),从edgeList中删除这条边 —— 此时两个单元格之间的墙壁被打通。
步骤 6:递归探索下一个单元格
DFS(nextX, nextY, edgeList, visited)
- 核心逻辑:调用自己(DFS 函数),以上一个单元格(nextX, nextY)为新起点,重复上述探索过程 —— 这就是 “递归” 的魔力,自动实现 “深入探索” 和 “回溯”。
3.用 “小故事” 理解 DFS 的探索与回溯
假设迷宫是一个 “房间群”,每个房间对应一个单元格:
- 你从 “起点房间 A”(0,39)出发,随机打开一扇门进入 “房间 B”(1,39),并锁上身后的门(标记为已访问);
- 在 “房间 B” 里,随机打开一扇未锁的门进入 “房间 C”(1,38),继续深入;
- 直到走到 “房间 Z”,发现所有门都已锁上(死胡同),就原路返回 “房间 Y”,尝试其他未锁的门;
- 重复这个过程,直到所有房间都走过 —— 此时迷宫就生成好了,没有重复路径,也没有回路。
五、最后一步:绘制迷宫并添加标记
DFS 生成迷宫后,我们需要绘制剩余的墙壁,再添加 “START” 和 “END” 标记,让迷宫更清晰易懂。这部分代码在if __name__ == "__main__":中,是程序的 “启动入口”,我们逐段拆解。
1.初始化绘图参数
首先设置绘图窗口的基本属性,确保迷宫显示效果清晰:
# 1. 初始化绘图参数
plt.figure(figsize=(12, 8)) # 设置窗口大小为12x8英寸,适配60x40迷宫
plt.axis('equal') # 保证x、y轴刻度比例一致,避免迷宫变形(如正方形变长方形)
plt.title('Maze (START: Top-Left | END: Bottom-Right)', fontsize=14, pad=15) # 设置标题,注明起点终点位置
figsize=(12,8):根据迷宫尺寸(60x40)设置窗口大小,避免迷宫显示过小或过大。plt.axis('equal'):核心参数 —— 如果不设置,x 轴和 y 轴的刻度比例可能不同,导致单元格变成 “长方形”,迷宫视觉上失真。pad=15:增加标题与迷宫之间的距离,避免拥挤。
2.初始化迷宫数据
调用前面定义的工具函数,创建 “完整网格” 和 “访问记录表”:
# 2. 初始化迷宫数据
edgeList = initEdgeList() # 创建所有墙壁的集合(完整网格)
visited = initVisitedList() # 创建访问记录列表(初始全为False)
3.设定起点和终点
根据matplotlib的坐标系(原点在左下角),明确迷宫的起点和终点位置:
# 3. 设定起点和终点
start_x, start_y = 0, HEIGHT-1 # 起点:左上角(x=0最左列,y=39最上行)
end_x, end_y = WIDTH-1, 0 # 终点:右下角(x=59最右列,y=0最下行)
- 关键理解:由于 y 轴向上递增,“最上行” 的 y 值是
HEIGHT-1(40 行的话就是 39),“最下行” 的 y 值是 0,这和我们直观的 “上高下低” 认知一致。
4.从起点开始执行 DFS 生成迷宫
标记起点为已访问,然后调用 DFS 函数开始探索和打通迷宫:
# 4. 从起点开始DFS生成迷宫
visited[start_y][start_x] = True # 标记起点为已访问(避免重复探索)
DFS(start_x, start_y, edgeList, visited) # 以起点为中心开始DFS探索
- 这里必须先标记
visited[start_y][start_x] = True,否则 DFS 可能会重复判断起点是否 “未访问”,导致逻辑混乱。
5.设置入口和出口(删除边界墙壁)
迷宫需要 “能进能出”,因此要删除起点和终点的边界墙壁,形成入口和出口:
# 5. 设置入口和出口(删除边界墙壁,与标记匹配!)
# 起点入口:删除左上角单元格的左边墙壁
start_left_edge = (start_x, start_y, start_x, start_y+1)
edgeList.discard(start_left_edge) # 用discard避免边不存在时报错
# 终点出口:删除右下角单元格的右边墙壁
end_right_edge = (end_x+1, end_y, end_x+1, end_y+1)
edgeList.discard(end_right_edge)
- 为什么用
discard而不是remove? 如果由于某种原因(如 DFS 已删除)边不在edgeList中,remove会报错,而discard会静默忽略,程序更稳健。 - 入口逻辑:起点
(0, 39)的左边墙壁是(0, 39, 0, 40),删除它后,外部可以通过这个缺口进入迷宫。 - 出口逻辑:终点
(59, 0)的右边墙壁是(60, 0, 60, 1),删除它后,迷宫内部可以通过这个缺口到达外部。
6.绘制迷宫墙壁
遍历edgeList中剩余的边,用黑色线条绘制出来 —— 这些边就是迷宫的 “墙壁”:
# 6. 绘制迷宫墙壁
for edge in edgeList:
drawLine(edge[0], edge[1], edge[2], edge[3])
- 此时
edgeList中只保留了未被 DFS 删除的边,绘制后就形成了迷宫的路径和墙壁。
7.标记 “START” 和 “END”(核心可视化优化)
为了让迷宫的起点和终点一目了然,我们添加文字标注和彩色圆点标记:
(1)标记起点 “START”(左上角)
# (1)起点标记:左上角(start_x=0, start_y=39)
# 文字标注:迷宫左侧,绿色加粗(绿色代表“开始”“安全”)
plt.text(
x=-2, # 位置在迷宫左边2个单位,避免遮挡迷宫
y=start_y + 0.5, # 与起点单元格垂直居中对齐(单元格中心y坐标=start_y+0.5)
s='START',
ha='center', # 文字水平居中
va='center', # 文字垂直居中
fontsize=8,
color='darkgreen',
fontweight='bold'
)
# 绿色圆点:标记在起点单元格中心(增强视觉识别)
plt.scatter(
x=start_x + 0.5, # 单元格水平中心(x坐标=start_x+0.5)
y=start_y + 0.5, # 单元格垂直中心(y坐标=start_y+0.5)
color='green',
s=80, # 圆点大小(数值越大,圆点越大)
alpha=0.8, # 透明度(0.8既醒目又不遮挡墙壁)
zorder=5 # 层级优先级(5确保圆点在墙壁上方显示,不会被遮挡)
)
(2)标记终点 “END”(右下角)
# (2)出口标记:右下角(end_x=59, end_y=0)
# 文字标注:迷宫右侧,红色加粗(红色代表“终点”“目标”)
plt.text(
x=WIDTH + 1, # 位置在迷宫右边1个单位
y=end_y + 0.5, # 与出口单元格垂直居中对齐
s='END',
ha='center',
va='center',
fontsize=8,
color='darkred',
fontweight='bold'
)
# 红色圆点:标记在出口单元格中心
plt.scatter(
x=end_x + 0.5,
y=end_y + 0.5,
color='red',
s=80,
alpha=0.8,
zorder=5
)
- 单元格中心计算:任何单元格
(x, y)的中心坐标都是(x + 0.5, y + 0.5),这是确保圆点精准落在单元格中心的关键。 zorder=5:matplotlib默认按绘制顺序确定层级,设置zorder可以强制让圆点在墙壁上方显示,避免被黑色墙壁遮挡。
8.隐藏坐标轴,优化视觉效果
删除 x 轴和 y 轴的刻度与线条,让迷宫更整洁:
# 8. 隐藏坐标轴,让迷宫更整洁
plt.axis('off')
9.显示迷宫
完成所有绘制后,弹出窗口显示最终的迷宫:
# 9. 显示迷宫
plt.tight_layout() # 自动调整布局,避免文字标注被窗口边缘截断
plt.show()
plt.tight_layout():解决文字标注(如 “START”“END”)可能被窗口边缘遮挡的问题,确保所有元素都能完整显示。
六、总结
DFS 迷宫生成的本质是 “先建全网格,再通过随机探索拆墙”:通过工具函数搭建基础框架,DFS 算法负责随机生成无回路路径,可视化模块将逻辑转化为直观图形。整个过程既体现了算法的严谨性,也通过可视化降低了理解门槛,是学习递归、DFS 算法及可视化编程的经典案例。

被折叠的 条评论
为什么被折叠?



