简介:这是面向C语言与数据结构课程设计的迷宫游戏升级版实现,适合需要完成类似题目或练习图遍历、路径搜索算法的在校学生。程序以控制台呈现迷宫地图,支持方向键操控老鼠走向粮仓,并包含定时成功/失败判定、迷宫编辑(墙变路、路变墙)以及全路径和最短路径求解功能,覆盖需求分析到编码实现的常见环节。压缩包共5个文件,约50KB:1个cpp为完整C++源代码,1个exe为可执行程序,3个txt为迷宫数据文件,分别对应不同地图布局,可替换使用以测试算法效果。算法上涉及图的遍历、回溯、广度/深度优先搜索等经典知识点,代码注释清晰,可帮助理解迷宫建模、路径存储与最短路径计算思路;同时提供可直接运行的exe,便于先体验再读源码,适合课程设计答辩前快速梳理方案。已有3889人学习,资源量不大但麻雀虽小五脏俱全,拿来当作课程设计参考或在此基础上扩展界面、增加难度都很有价值。
1. 老鼠走迷宫游戏升级版到底在考什么:从“能跑通”到“敢答辩”
期末课设答辩现场,你刚把老鼠走出迷宫的截图放出来,老师随手一指地图右下角:“你这只老鼠从头到尾绕了个大弯,我用眼睛都能看出最短路径不是这个。你写的是深度优先,它凭什么叫‘优先’?”这个标题里的【老鼠走迷宫游戏升级版课程设计(c语言+数据结构)源代码+迷宫文件】,说的就是应付这类追问的项目:老鼠走迷宫是数据结构课设里出现频率最高的一题,升级版通常意味着不只有单条路径,还要支持随机生成迷宫、求解并展示路径。它把栈、队列、图遍历、文件读写全串进一个程序,适合正在赶课设、或者想把项目从“能运行”打磨到“能解释”的人。这篇我按自己做课设和帮别人改代码的顺序,从迷宫文件格式讲到寻路算法,再落到避坑,全文基于 C 语言实现。
2. 迷宫从哪来:手工地图、随机生成与迷宫文件格式设计
很多人的第一版迷宫是直接在代码里写死一个二维数组,8×8 跑通就交差。但老师一眼就能看出来:地图写死在代码里,换不了图,也谈不上“游戏”。升级版的第一个升级点,就是把迷宫“数据化”——地图放在文件里,程序只负责读。这样换题、换难度、答辩演示都方便。
2.1 先定迷宫文件格式:0 和 1 之外的三个细节
我一般用的格式很简单:第一行两个整数,表示行数和列数;后面每行是若干个 0 或 1,0 代表可以通过的路,1 代表墙。这个格式的好处是能用记事本直接画图,也能用脚本批量生成。下面是完整的读取函数,建议直接放进maze_io.c:
#include <stdio.h> #include <stdlib.h> #define MAX_ROW 64 #define MAX_COL 64 // 从文件读取迷宫,成功返回 1,失败返回 0 int loadMaze(const char *filename, int maze[MAX_ROW][MAX_COL], int *row, int *col) { FILE *fp = fopen(filename, "r"); if (fp == NULL) { printf("打不开 %s\n", filename); return 0; } // 先读行数和列数 if (fscanf(fp, "%d %d", row, col) != 2) { fclose(fp); return 0; } if (*row > MAX_ROW || *col > MAX_COL) { printf("迷宫尺寸超过 %d x %d\n", MAX_ROW, MAX_COL); fclose(fp); return 0; } // 逐个数读 0/1,跳过空白符 for (int i = 0; i < *row; i++) { for (int j = 0; j < *col; j++) { if (fscanf(fp, "%d", &maze[i][j]) != 1) { fclose(fp); return 0; } } } fclose(fp); return 1; }逻辑上和文件格式一一对应:先读两个尺寸,再按行读迷宫矩阵,任何一个数字读不到就直接返回 0,避免程序带着残缺数据往下跑。参数MAX_ROW和MAX_COL要和后面生成、求解用的数组声明保持一致,我习惯统一写在头文件里;迷宫最大能开多大,取决于你栈上数组怎么声明,64×64 是课设够用的起点,超过 128×128 建议改成动态数组。
这里有个容易被忽略的细节:文件里 0 和 1 之间用什么分隔其实无所谓,因为fscanf遇到空格、换行、制表符都会自动跳过。我之前见过同学用fgets逐行读再手动处理,结果每次换行符都多吞一个字符,地图整体错位。另一个细节是行列数最好用int变量带回来,而不是全局变量,这样同一个函数可以加载多张地图做对比。第三个细节是文件结尾不要有多余的空白行,有些编辑器会自动补一个空行,虽然fscanf能跳过,但手工检查文件时会干扰判断,所以建议统一在格式说明里写清楚“末尾不留空行”。
2.2 用 DFS 回溯随机生成迷宫:挖墙法的原理与参数
手工编辑文件适合做“演示地图”,但课设老师更想看到“程序自己生成地图”。常见生成算法是递归回溯挖墙法,原理一句话:把迷宫当成一块块方格,初始全部是墙,从起点格子出发,每隔一个格子打通一条通道,随机挑方向往前走,走到没路就退回上一个格子继续试。这样生成的迷宫天然满足“任意两个空地之间只有一条通路”的特性,正好和后面求解用的栈结构形成对照。
我一般用奇数尺寸的二维数组,比如 41 行 41 列,外围保留一圈墙,然后从 (1,1) 开始挖。核心代码如下:
#include <time.h> #include <stdlib.h> // 跳两格的方向偏移:上、右、下、左 int dig_dx[4] = {-2, 0, 2, 0}; int dig_dy[4] = {0, 2, 0, -2}; // 洗牌,让每次生成的迷宫不一样 void shuffle(int *arr, int n) { for (int i = n - 1; i > 0; i--) { int j = rand() % (i + 1); int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } // 从 (x, y) 开始递归挖墙,调用前先把 maze 全部置 1 void dfsGenerate(int maze[MAX_ROW][MAX_COL], int x, int y) { maze[x][y] = 0; // 挖开当前格子 int order[4] = {0, 1, 2, 3}; shuffle(order, 4); // 随机方向顺序 for (int i = 0; i < 4; i++) { int nx = x + dig_dx[order[i]]; int ny = y + dig_dy[order[i]]; // 隔一个格子判断,防止打出边界 if (nx > 0 && nx < MAX_ROW - 1 && ny > 0 && ny < MAX_COL - 1 && maze[nx][ny] == 1) { // 把中间的墙也挖开 maze[x + dig_dx[order[i]] / 2][y + dig_dy[order[i]] / 2] = 0; dfsGenerate(maze, nx, ny); } } }这段代码的关键是dig_dx和dig_dy每次跳两格而不是一格:跳的那一格是要被挖开的墙,跳两格后才到达下一个格子。为什么不能每次走一格?因为按格子逐个挖,生成的会是一团乱麻,几乎没有墙,也没有迷惑性。调用前需要先srand(time(NULL))初始化随机种子,否则每次生成的地图都一样,答辩时会被当成“假随机”。参数上,MAX_ROW和MAX_COL必须是奇数,因为起点是 (1,1),跳两格后仍然是奇数坐标,能保证通道格子都在奇数行奇数列;如果你传了偶数尺寸,最后会剩下几排墙没法处理。
递归生成法最怕的是层数太深。迷宫越大,递归调用栈越深,Windows 默认 1MB 栈空间下超过一千层就有概率爆栈。这也是一个适合写进课设报告的点:递归版本简洁,但显式栈版本能承受更大的迷宫。如果你想在代码层面解决,可以把递归改成手动栈,或者限制迷宫尺寸在 127×127 以内,基本安全。
2.3 对比 PRIM 生成法:选型看特征,别跟风
除了 DFS 挖墙,另一个常见生成法是 PRIM 算法。它维护一个“墙列表”,随机挑一堵墙,判断墙两边的格子是否一个已打通、一个未打通,满足条件就把这堵墙挖开。PRIM 生成出来的迷宫分叉多、支路均匀,不会出现 DFS 那种一条长走廊走到黑的情况。但是实现上要额外维护墙的集合,代码量比递归挖墙大一圈。
如果你只想要“能生成迷宫”,我建议选 DFS 挖墙,代码短、好讲;如果你追求“迷宫好看”,可以在课设报告里写清楚两种算法的差异,然后用 PRIM 做加分项。整理一张对比表:
| 生成算法 | 特征 | 代码量 | 适用场景 |
|---|---|---|---|
| DFS 递归挖墙 | 路径偏长、分支较少 | 小 | 课设够用,重点讲回溯 |
| PRIM 挖墙 | 分支均匀、路线多样 | 中 | 想加分的同学 |
| 纯随机布墙 | 可能无解 | 极小 | 不建议作为主方案 |
纯随机布墙就是每个格子按概率置 0/1,这种做法经常出现整块死区,老鼠根本走不到出口,还得额外做连通性检查,不如直接用上面两种有理论保证的算法。
2.4 把迷宫写回文件:存档与读档的最小实现
生成完迷宫不能只存在内存里,否则程序一关图就没了。写回文件和读文件是对称的,核心是fprintf按同样的格式输出:
// 把迷宫保存到文件,成功返回 1 int saveMaze(const char *filename, int maze[MAX_ROW][MAX_COL], int row, int col) { FILE *fp = fopen(filename, "w"); if (fp == NULL) return 0; fprintf(fp, "%d %d\n", row, col); // 先写尺寸 for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { fprintf(fp, "%d ", maze[i][j]); // 0/1 后面跟空格 } fprintf(fp, "\n"); } fclose(fp); return 1; }逻辑上先写行列数再写矩阵,和loadMaze正好互逆。这里有个课设实操建议:随机生成一张 61×61 的迷宫,直接saveMaze("maps/demo.txt", maze, row, col)保存,然后在主程序里用loadMaze重新加载,既演示了生成,又演示了文件读写,比口头说“我支持文件加载”更有说服力。文件命名建议统一放到maps/目录,避免和源码混在一起,答辩时也更好找。
3. 寻路核心:栈、递归与 BFS 最短路径的落地写法
迷宫生成完,真正的重头戏是求解。数据结构课设里老师最关注的,就是你能不能把栈和队列这两个“线性结构”用到具体问题上。老鼠走迷宫最经典的解法是深度优先搜索,它天然对应栈;升级版要求的最短路径,则对应队列。这一章把两条路线都写清楚。
3.1 递归和栈到底什么关系:老师追问的底层问题
先回答一个答辩高频问题:递归是不是栈?递归不是栈,但递归调用依赖系统栈。每次调用函数,系统会把当前函数的局部变量、返回地址压入调用栈;一层层递归下去,栈就越压越深。网上有个热梗叫“单片机 c 语言没有堆栈吗为什么”,其实不是没有堆栈,而是单片机默认给的栈空间太小,递归稍微深一点就栈溢出,直接硬件异常。你在 PC 上写课设,1MB 栈跑 64×64 迷宫没问题,但老师真正想听的是:你知道递归背后要消耗栈空间,并且知道怎么把递归改成手动栈。
这就引出升级版的常见加分点:同一个 DFS 求解,先写递归版本,再写显式栈版本,在报告里对比两种实现的栈占用。递归版的优点是代码短,缺点是每个递归帧有函数调用开销,而且没法在深搜过程中随时“跳出来”做状态展示;显式栈版把状态全部捏在自己手里,想暂停、想演示、想记录每一步都更方便。
3.2 用显式栈实现 DFS:回溯的路径记录与参数调优
显式栈做 DFS 的思路是:栈里保存“当前走到哪个格子、下一步该尝试哪个方向”。每走一步就压栈,四个方向都走不通就把栈顶弹出——弹出的动作就是“回溯”。我用的数据结构是结构体数组:
#define MAX_STACK 8192 typedef struct { int x, y; // 当前坐标 int dir; // 下一个要尝试的方向下标 0~3 } Step; // 单步方向:上、右、下、左 int mv_dx[4] = {-1, 0, 1, 0}; int mv_dy[4] = {0, 1, 0, -1}; // 显式栈 DFS 求解,找到路径返回 1,并把路径标记到 path 数组 int solveWithStack(int maze[MAX_ROW][MAX_COL], int row, int col, int sx, int sy, int ex, int ey, int path[MAX_ROW][MAX_COL]) { Step stack[MAX_STACK]; int visited[MAX_ROW][MAX_COL] = {0}; int top = -1; stack[++top] = (Step){sx, sy, 0}; visited[sx][sy] = 1; while (top >= 0) { Step *cur = &stack[top]; if (cur->x == ex && cur->y == ey) { // 从栈底到栈顶就是完整路径 for (int i = 0; i <= top; i++) { path[stack[i].x][stack[i].y] = 1; } return 1; } int found = 0; while (cur->dir < 4) { int nx = cur->x + mv_dx[cur->dir]; int ny = cur->y + mv_dy[cur->dir]; cur->dir++; // 方向指针自增,回溯时接着试下一个 if (nx >= 0 && nx < row && ny >= 0 && ny < col && maze[nx][ny] == 0 && !visited[nx][ny]) { stack[++top] = (Step){nx, ny, 0}; visited[nx][ny] = 1; found = 1; break; // 找到路就立即深入 } } if (!found) { top--; // 四个方向都试完,回退一格 } } return 0; // 栈空,没有通路 }这段代码里最重要的字段是dir。它记录“这个格子下一步该试哪个方向”,因为回溯时不能从头再试,否则会死循环。每尝试一个方向就cur->dir++,就算这个方向走不通,下次回到这个栈顶元素时也会从下一个方向继续,而不是重新从 0 开始。visited数组防止走回头路,否则老鼠会在两个空格之间来回踩,栈越压越满。
参数方面:MAX_STACK开多大取决于迷宫大小。64×64 的迷宫路径最长也就几千步,8192 足够;如果迷宫开到 200×200,建议把栈声明成动态数组,或者至少 20000 起步。另外注意我传入了row和col,而不是直接用MAX_ROW,这样同一个函数可以在不同尺寸的迷宫上复用,代码更规范。
显式栈版本跑通后,倒回去看递归版本就是同一件事:递归函数自己压栈,dir体现在 for 循环的i上。两种实现的最终路径一样,但显式栈可以在每一步输出当前栈的内容,这是做“路径搜索过程可视化”的基础。
3.3 BFS 求最短路径:手写队列与前驱数组恢复路径
升级版最明显的功能就是“最短路径”。DFS 找到一条路径就收工,不保证最短;想拿最短路径要用 BFS,也就是一层一层往外扩散。BFS 天然对应队列,先进先出,先到达出口的层数就是最短步数。
typedef struct { int x, y; } Point; // BFS 求 (sx, sy) 到 (ex, ey) 的最短路径,返回步数;不可达返回 0 int bfsShortest(int maze[MAX_ROW][MAX_COL], int row, int col, int sx, int sy, int ex, int ey, int path[MAX_ROW][MAX_COL]) { Point queue[MAX_ROW * MAX_COL]; int dist[MAX_ROW][MAX_COL]; Point pre[MAX_ROW][MAX_COL]; int head = 0, tail = 0; for (int i = 0; i < row; i++) for (int j = 0; j < col; j++) dist[i][j] = -1; // -1 表示还没访问过 queue[tail++] = (Point){sx, sy}; dist[sx][sy] = 0; while (head < tail) { Point cur = queue[head++]; if (cur.x == ex && cur.y == ey) break; for (int k = 0; k < 4; k++) { int nx = cur.x + mv_dx[k]; int ny = cur.y + mv_dy[k]; if (nx >= 0 && nx < row && ny >= 0 && ny < col && maze[nx][ny] == 0 && dist[nx][ny] == -1) { dist[nx][ny] = dist[cur.x][cur.y] + 1; pre[nx][ny] = cur; // 记下“从哪来” queue[tail++] = (Point){nx, ny}; } } } if (dist[ex][ey] == -1) return 0; // 不可达 // 从出口倒着走回入口,恢复路径 Point cur = {ex, ey}; while (cur.x != sx || cur.y != sy) { path[cur.x][cur.y] = 1; cur = pre[cur.x][cur.y]; } path[sx][sy] = 1; return dist[ex][ey] + 1; // +1 是因为入口也算一步 }这里我用手写数组当队列,tail负责入队,head负责出队,队空条件是head >= tail。为什么手写而不是用标准库的队列?因为课设要求“用数据结构”,手写队列能展示你对先进先出的理解,而且答辩时老师一定会问“队列怎么实现的”,手写版本可以直接讲清楚head和tail的滑动。dist数组记录每个格子到起点的最短步数,同时也是访问标记,-1表示未访问。pre数组是“前驱”,记录每个格子是从哪个格子走过来的,最后从出口反向回溯到入口,就能把最短路径标出来。
三个数组的职责要分清:dist算距离、pre恢复路径、path输出。初学者常犯的错误是把pre省掉,最后输出不了路径;或者只用visited标记,却不知道步数。BFS 返回值的单位也要注意:dist[ex][ey]是从起点到出口走了多少步,加上起点本身,路径格子数就是“步数 + 1”。如果你在报告里写“最短路径长度为 12”,必须说清楚是 12 步还是 12 个格子,别再小细节上被扣分。
4. “升级版”升级在哪:多路径收集、动画演示与交互菜单
做完生成和求解,一个 60 分的课设已经成型。但“升级版”三个字意味着还要再加点东西:打印所有路径、把搜索过程做成动画、做一个能循环操作的控制台菜单。这些不是算法题,是工程题,写起来不难,但对答辩观感提升很大。
4.1 打印所有可行路径:访问标记撤销是唯一难点
DFS 只找一条路径,因为找到出口就 return。想收集所有路径,做法是把 return 改成“记录一条,继续找”,同时保证已经为当前解设置的访问标记在返回前被撤销。看下面的代码:
int allPathCount = 0; // 收集从 (x, y) 到 (ex, ey) 的所有路径 void collectAllPaths(int maze[MAX_ROW][MAX_COL], int row, int col, int x, int y, int ex, int ey, int visited[MAX_ROW][MAX_COL]) { if (x == ex && y == ey) { allPathCount++; printf("第 %d 条路径:(%d, %d) 到出口\n", allPathCount, x, y); return; } for (int k = 0; k < 4; k++) { int nx = x + mv_dx[k]; int ny = y + mv_dy[k]; if (nx >= 0 && nx < row && ny >= 0 && ny < col && maze[nx][ny] == 0 && !visited[nx][ny]) { visited[nx][ny] = 1; // 进入前标记 collectAllPaths(maze, row, col, nx, ny, ex, ey, visited); visited[nx][ny] = 0; // 返回后撤销,关键! } } }唯一的难点就在visited[nx][ny] = 0;这一行。撤销标记意味着:这个格子虽然在“方案 A”里走过了,但换一条分支时,它还能重新被走。如果把撤销去掉,第一次深搜会把所有走过的格子永久标记,后续分支全部被堵死,收集到的路径数会少很多。注意“打印所有路径”的开销是指数级的,64×64 的迷宫路径数量可能非常庞大,所以这个功能只适合在小迷宫上演示,比如 9×9 的手工图。答辩演示用 9×9,性能演示用 41×41 的 BFS,两个场景分开,效果最好。
4.2 让求解过程动态化:延时、清屏与光标定位
纯控制台程序最直观的升级是动画:老鼠一步一步往前走,每一步把画面重画一遍。Windows 下的做法是system("cls")清屏,加上Sleep延时,再用一个光标定位函数把老鼠画在指定位置。注意这段代码依赖 Windows API,Linux/macOS 用户可以用 ANSI 转义序列替代:
#include <windows.h> // Sleep、system 都在这个头文件里 // 按当前坐标和迷宫状态刷一帧画面 void render(int maze[MAX_ROW][MAX_COL], int row, int col, int cur_x, int cur_y) { system("cls"); // 清屏,重新画 for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { if (i == cur_x && j == cur_y) { printf("@"); // 当前老鼠位置 } else if (maze[i][j] == 1) { printf("#"); // 墙 } else { printf(" "); // 空地 } } printf("\n"); } Sleep(80); // 每帧 80ms,太快看不清,太慢老师着急 }调用时机放在 BFS 或 DFS 每走一步之后,也就是循环里面每访问一个新格子就调用一次render。参数上,Sleep(80)是最常用的值,60 到 120 之间都可以;小于 30 毫秒人眼基本跟不上,大于 200 毫秒会显得程序很卡。清屏方式在 Windows 控制台可以直接用system("cls"),但注意system调用会频繁拉起子进程,有性能损耗;如果以后想跨平台,建议改用光标定位加\033[2J的 ANSI 序列。这一点写进课设报告里,能体现你考虑过可移植性。
4.3 菜单循环与存档读档:让课设看起来像一个作品
最后一个工程化点是主菜单。不要用“一个 main 函数从头跑到尾”的方式,而是做一个while(1)循环,用户输入数字选择功能:
int main(void) { int maze[MAX_ROW][MAX_COL] = {0}; int row = 0, col = 0; int choice = 0; while (1) { printf("\n==== 老鼠走迷宫课设 ====\n"); printf("1. 从文件读取迷宫\n"); printf("2. 随机生成迷宫并保存\n"); printf("3. DFS 显示一条路径\n"); printf("4. BFS 显示最短路径\n"); printf("5. 统计所有路径数量\n"); printf("0. 退出\n"); printf("请选择:"); if (scanf("%d", &choice) != 1) break; if (choice == 0) break; if (choice == 1) { char name[64]; printf("输入文件名:"); scanf("%s", name); if (!loadMaze(name, maze, &row, &col)) printf("加载失败\n"); else printf("加载成功:%d 行 %d 列\n", row, col); } else if (choice == 2) { // 初始化全墙 -> dfsGenerate -> saveMaze printf("随机生成功能,初始化后调用 dfsGenerate\n"); } else if (choice == 3) { // 调用 solveWithStack,然后打印路径图 } else if (choice == 4) { // 调用 bfsShortest,打印路径和步数 } } return 0; }菜单不是核心算法,但能把前面所有函数串起来。尤其建议把“随机生成并保存”和“从文件读取”做成两个互相独立的功能,答辩时老师让你现场生成一张新迷宫,你保存后再读回来,整个过程非常完整。scanf的返回值也要检查,否则输入字母时缓冲区残留会导致菜单死循环,这一点在下一章的避坑里细说。
5. 老鼠走迷宫课设避坑指南:5 个最常见的翻车现场
这一章是我帮别人改课设时实际踩过的坑,每一个都足够让程序在答辩现场崩溃,按现象、原因、解决三步写清楚,建议把这一节内容直接并入你的课设报告“调试过程”部分。
5.1 数组越界:行和列写反,墙没包边
现象:程序一运行就报“内存访问冲突”,或者迷宫打印出来第一行正常、后面全部错位。
原因:最常见的是把maze[x][y]的下标顺序写反。我习惯用x表示行、y表示列,但文件里写的是“行数 列数”,读文件时如果两层循环写成i < col,数组就会越界。另一个原因是生成迷宫时没有保留外围一圈墙,DFS 从边缘格子出发,nx < 0直接访问负下标。
解决:读取和生成时都统一用“行、列”的顺序,并给迷宫加一圈 1 的墙。我一般在数组声明上多留两行两列,外围强制置 1,内部才允许挖开。写完读函数后先用 3×3 的小迷宫跑一遍,快速肉眼检查,别一上来就跑 64×64。
5.2 回溯时不撤销访问标记,把路“焊死”
现象:函数能跑,但打印出来的路径只有一条,而且明显绕远;或者“打印所有路径”功能只输出 1 条。
原因:DFS 里visited[nx][ny] = 1之后,递归返回时没有恢复为 0。这样第一次深搜走过的所有格子都被永久标记,后面的分支再也进不去。这是“回溯”两个字里最容易丢的动作。
解决:每次递归调用返回后,立刻把visited[nx][ny] = 0还原。可以把这个写代码的顺序固定下来:先写进入标记,再写递归调用,最后补还原,顺序不要颠倒。显式栈版本里对应的坑是出栈时忘了清visited,同样会让搜索提前结束。
5.3 读文件多读一个换行符,迷宫整体错位
现象:同一个文件,别人读是对的,自己读出来最后一行多一串 0,或者行数少 1。
原因:用fgets逐行读再手动按字符解析时,Windows 文件每行结尾是\r\n,fgets会把\n保留在缓冲区里,sscanf跳过它是没问题的,但如果你用strlen数长度、再按字符判断,就会把\r当成一个数字的一部分。还有同学在fscanf读完后顺手加一个fscanf(fp, "\n"),反而吞掉了下一行第一个数字。
解决:迷宫数字读取全部交给fscanf("%d"),它天然跳过所有空白字符,不要手动处理换行。要检查是否读完,只判断fscanf返回值是不是 1,而不是判断文件指针到了哪一行。这是最省心的做法,也是我坚持用fscanf而不是fgets的原因。
5.4 求解直接改迷宫地图,导致第二次求解失败
现象:第一次求解成功,路径用*或#画出来;第二次再求解,程序要么找不到路,要么路径全是*。
原因:很多同学直接把路径标记写进maze数组,把原本是 0 的空地改成了 2 或*。第二次搜索时,maze[nx][ny] == 0的判断永远不成立。
解决:路径标记单独用一个path数组,求解函数只读maze、只写path。这样迷宫地图保持只读,可以反复求解,也可以在做完 BFS 后立刻做 DFS,互不干扰。如果一定要让路径显示在原图上,也得先复制一份maze,在副本上操作。
5.5 一条可行路径都没有:起点终点与不可达判断
现象:生成的迷宫看似正常,但求解函数返回 0,程序直接卡住或什么都不显示。
原因:很可能是起点或终点坐标本身就是墙,也可能是随机生成时出口角落被堵死。DFS 挖墙生成的迷宫理论上全连通,但如果你把某个格子手动改成墙,或者 PRIM 实现有 bug,就会产生不可达区域。
解决:求解前先做两件事:检查maze[sx][sy] == 0 && maze[ex][ey] == 0;然后调用一次 BFS,如果返回 0 直接提示“出口不可达”,而不是一头扎进 DFS 死循环。把不可达判断单独写成一个函数,课设报告里可以写“本程序具备可达性校验能力”,这句话比“我调通了”更有分量。
6. 用三种方法验证你的求解器:人眼、交叉算法与自动比对
求解器写完,怎么证明它是对的?我习惯三种方法一起上,从快到慢排。
第一种是人眼对照:把迷宫原图打印出来,再打印带路径的图,两张并排看。路径必须是一条连续的 4 连通通路,从入口到出口,中间没有穿过墙。这种方法对小迷宫最快,但大迷宫人眼容易看花,而且只能证明“有一条路”,证明不了“是最短路径”。
第二种是交叉算法验证:用 DFS 和 BFS 分别求解同一张迷宫,DFS 找到的路径长度一定大于等于 BFS 返回的最短步数。如果 DFS 结果比 BFS 短,那一定有一方写错了;如果 BFS 返回 0 而 DFS 有输出,问题出在坐标或边界条件。这个对照不用写额外代码,菜单里已经有两个功能,跑两次对比就行。
第三种是自动比对:把求解结果写回文件,写一个简单的检查函数,遍历路径数组,验证三点——起点和终点被标记;路径上每个格子都是maze == 0;相邻两个路径点之间曼哈顿距离为 1。这一步看起来麻烦,但一旦迷宫尺寸加大,人眼完全不可靠,自动化检查是唯一能兜底的方案。我现在的习惯是每改一次寻路逻辑,先跑一遍自动检查再继续下一个功能。
最后说一个答辩时的小技巧:不要只在控制台闪一遍结果,提前准备好三张图——一张原迷宫、一张 DFS 路径、一张 BFS 最短路径,并排放在报告里,标注清路径长度。老师问到算法复杂度时,直接答 DFS 最坏 O(行×列)、BFS 同样 O(行×列),空间上 BFS 的队列最多存整张图。这也是我对每个迷宫数据的第一反应:先检查可达性,再谈算法对比。希望帮到你。
本文还有配套的精品资源,点击获取