Qt迷宫生成与路径搜索实战:BFS/A*算法及可视化实现
简介Qt随机迷宫及路径获取工程是一份结合Qt图形界面、并查集与A寻路算法的完整示例适合Qt学习者和算法入门者参考。资源为RAR压缩包共66个文件以cpp/h源文件、exe可执行程序、dll运行库、png位图资源及qss样式文件为主整体约28.14MB解压后可直接打开解决方案进行编译运行或体验现有程序。压缩包内包含VS工程所需的各类依赖与资源文件无需额外配置即可搭建开发环境。目前已有353人浏览学习工程文件按QtMaze项目组织目录划分清晰便于按模块阅读源码。项目中利用并查集高效处理迷宫格子的连通关系配合深度优先搜索生成随机迷宫再通过A算法动态计算最短路径并高亮显示同时支持鼠标点击格子重新规划路径可直观理解二维数组迷宫建模、QGraphicsView绘图和启发式搜索的协作流程。资源附带完整源码与可执行程序读者既能查看算法实现也能直接运行观察效果是GUI程序设计练习和数据结构与算法落地的优秀参考。1. 随机迷宫与路径获取不只是算法 Demo也是 Qt 可视化应用的底盘打开程序点一下按钮窗口里立刻长出一张随机迷宫再点一下一条从入口到出口的红线被画出来——这是「Qt随机迷宫及路径获取」这类项目最典型的样子。但它背后实际打通的是三条链路用二维数组描述迷宫的地图数据用 QPainter 把数组渲染到窗口再用 BFS 或 A* 这类图搜索算法把路径从地图里抠出来。这个方向特别适合两类人一类是刚掌握 C 基础、想找个带界面的练手项目的新手另一类是做小游戏、地图编辑器或算法可视化工具的从业者需要一个自带 UI 的迷宫生成与寻路底座。本文会从数据模型讲到绘制再讲到寻路实现和使用技巧全程按可复现的方式组织。2. 迷宫生成的底层逻辑数据模型与两类经典算法先回答一个常被忽略的问题在写任何绘图代码之前迷宫应该以什么形式存在常见做法是把它当成一张网格图。你不需要模拟一面墙的三维几何只需要回答每个格子和它的上下左右邻居之间「通不通」。这个信息用一个二维数组就能完整表达。2.1 用 Cell 存四方向墙二维数组的坐标约定每个格子保存四面墙是否存在外加一个 visited 标记给生成算法用。四个方向我习惯按顺时针顺序编号北 0、东 1、南 2、西 3。enum Direction { NORTH 0, EAST, SOUTH, WEST }; struct Cell { bool walls[4] { true, true, true, true }; bool visited false; // 生成算法使用寻路阶段不依赖它 }; // dr/dc 分别表示向某个方向移动时行和列的增量 const int dr[4] { -1, 0, 1, 0 }; // N, E, S, W const int dc[4] { 0, 1, 0, -1 }; // N, E, S, W为什么用 4 个 bool 而不是一个 unsigned char 的位运算纯性能上位运算更省但迷宫生成和寻路阶段频繁读写墙状态bool 数组的代码更直白可读性优先。等到你真要跑 1000x1000 的迷宫、内存吃紧时再压缩成 char 不迟。坐标约定是整篇文章最容易翻车的地方务必在一开始就定死maze[row][col]的第一维是行纵向第二维是列横向。而在 Qt 的 QPainter 里x 是横向、y 是纵向。两套坐标系叠加后像素坐标与数组下标的关系如下表位置语义范围maze[r][c]第 r 行第 c 列的格子0 ≤ r rows0 ≤ c colsQPoint(r, c)第一个数是行号第二个数是列号与数组下标一致像素坐标offsetX c * celloffsetY r * cell列号做横坐标行号做纵坐标QPoint虽然叫 Point但在迷宫上下文里我用它装(row, col)而不是像素坐标。这个约定要写进头文件注释不然过两周自己看都发蒙。注意整个项目只允许存在一种坐标约定。不要在 QPoint 里一会儿放 (row, col)一会儿放 (col, row)。路径搜索和渲染都会因此错位后面排查的成本比写代码高得多。2.2 递归回溯生成迷宫显式栈与随机种子递归回溯是「深度优先」思想从起点格子出发标记已访问随机挑一个未访问的邻居拆墙走过去走到无路可走就退回上一层。它生成的迷宫是典型的「完美迷宫」任意两个格子之间恰好只有一条通路路径可视化出来非常干净非常适合接寻路演示。但教科书版递归在 200x200 的迷宫里递归深度可能达到几万层超过默认线程栈空间直接崩溃。所以工程上我会直接用显式栈把它改写成迭代版本一行递归都不写// maze: Cell 二维数组, rows/cols: 行列数, seed: 随机种子 void generateMaze(QVectorQVectorCell maze, int rows, int cols, quint32 seed) { QRandomGenerator rng(seed); // 局部随机数生成器固定种子可复现 QStackQPairint, int stack; int startR 0, startC 0; maze[startR][startC].visited true; stack.push({startR, startC}); while (!stack.isEmpty()) { auto cur stack.top(); int row cur.first, col cur.second; // 收集周围未访问的邻居方向 QVectorint candidates; for (int d 0; d 4; d) { int nr row dr[d], nc col dc[d]; if (nr 0 nr rows nc 0 nc cols !maze[nr][nc].visited) { candidates.append(d); } } if (candidates.isEmpty()) { stack.pop(); // 死胡同回溯 continue; } int d candidates[rng.bounded(candidates.size())]; int nr row dr[d], nc col dc[d]; // 拆掉当前格与目标格之间的墙反向墙必须同步拆 maze[row][col].walls[d] false; maze[nr][nc].walls[(d 2) % 4] false; maze[nr][nc].visited true; stack.push({nr, nc}); } }这段代码的参数和关键行为要展开说。rows和cols决定迷宫规模配合界面宽高比一般取 30x30 到 100x100超过 300x300 首屏生成耗时和绘制压力都明显上升。seed是调试期的救命参数同一个 seed 永远生成同一张迷宫复现问题就靠它。rng.bounded(n)返回 [0, n) 的随机整数比直接取模% n分布更均匀能避免随机方向偏向某个区域的视觉异常。拆墙那两行是整个生成算法的灵魂当前格往东拆了东墙目标格必须同时拆掉它的西墙(d 2) % 4就是方向取反。漏了这一步迷宫会生成出一堆「单边墙」视觉上像是左右两半各画各的寻路时一查墙就矛盾。注意用局部QRandomGenerator(seed)而不是全局QRandomGenerator::global()。前者是确定性的后者每次启动的随机序列都不同会让「复现迷宫」彻底变成玄学。2.3 换一种生成思路Prim 算法生成的迷宫有什么不同Prim 是另一种经典生成算法和 DFS 的核心差异在于「扩展策略」。DFS 是一条路走到黑再回头迷宫会呈现明显长走廊Prim 维护一个候选墙集合每次从集合里随机取一面墙如果墙对面的格子还没被访问就拆墙把那个格子并入已访问区域同时把它四周的墙加入候选集合。Prim 生成的迷宫分支更均匀不会出现 DFS 那种快到出口、但一路都在绕的感觉。实现要点有三条第一候选墙要存「墙的两端格子」不能只存一个方向第二拆墙后同样要对称修改两端格子的墙状态第三set 或 vector 存候选墙都可以随机取时注意别让取出的墙重复参与判断。完整的 Prim 工程实现大概四十行这里不展开贴因为对「Qt随机迷宫及路径获取」这个项目来说DFS 已经满足需求Prim 更多是当你觉得 DFS 迷宫太直、想换手感的备选方案。两种算法的选型总结成一个表方便对照维度DFS 递归回溯显式栈Prim核心结构栈 邻居随机候选墙集合迷宫形态长走廊多路径陡峭分支均匀视觉平均实现量约 30 行约 40 行任意两点唯一路径是完美迷宫是完美迷宫首屏生成速度快略慢但都可忽略对于本标题的演示场景我默认用 DFS。等界面、寻路都跑通了再回来把 generateMaze 替换成 Prim 版本正好能作为代码设计的「换算法不改接口」验证。3. 把迷宫画到 Qt 窗口绘制坐标与重绘节奏数据模型有了下一步是把二维数组变成用户能看到的墙和路径。这里核心是两件事坐标换算别出错重绘策略要扛得住 resize。3.1 用 QPainter 绘制格子墙壁只画北墙与西墙最直觉的画法是遍历每个格子画四面墙代码最短但相邻格子之间的同一面墙会被画两次。QPen 宽度较大时重叠处颜色明显发深窗口边缘和内部墙线深浅不一视觉上很掉价。我常用的约定是每个格子只负责自己的北墙和西墙南墙和东墙交给下面和右面的邻居最后再补一圈外框。void MazeWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.fillRect(rect(), Qt::white); const int rows maze.size(); const int cols maze[0].size(); const int cell qMin(width() / cols, height() / rows); // 保证格子是正方形 const int offsetX (width() - cell * cols) / 2; // 水平居中 const int offsetY (height() - cell * rows) / 2; // 垂直居中 QPen pen(QColor(30, 30, 30), 2); painter.setPen(pen); for (int r 0; r rows; r) { for (int c 0; c cols; c) { int x offsetX c * cell; int y offsetY r * cell; const Cell ref maze[r][c]; if (ref.walls[NORTH]) painter.drawLine(x, y, x cell, y); // 北墙 if (ref.walls[WEST]) painter.drawLine(x, y, x, y cell); // 西墙 } } // 补外框最底下一行的南墙 for (int c 0; c cols; c) { int x offsetX c * cell; int y offsetY rows * cell; painter.drawLine(x, y, x cell, y); } // 补外框最右边一列的东墙 for (int r 0; r rows; r) { int x offsetX cols * cell; int y offsetY r * cell; painter.drawLine(x, y, x, y cell); } }这里每个变量都要说清楚。cell qMin(width() / cols, height() / rows)保证格子是正方形而不是用独立的 cellW 和 cellH 把格子拉成矩形——迷宫格子一旦是长方形路径画出来会跟着扁视觉上像被拉伸过。offsetX/offsetY负责把迷宫整体居中避免窗口比迷宫宽时所有墙挤在左上角。内层循环只画北墙和西墙意味着最上面一行的北墙和最左边一列的西墙由格子自己画最下面一行和最右边一列则需要外框循环补上。这种「每面墙只画一次」的策略在墙线宽度调到 3、4 时优势很明显边界颜色均匀。3.2 起点、终点与默认路径的绘制叠加迷宫画完自然要画「从入口到出口的路径」。默认起点在左上角 (0, 0)终点在右下角 (rows-1, cols-1)。路径的绘制我用「格子中心折线」把路径上每个格子的中心点连成一条多段线红线从入口一路延伸到出口。void MazeWidget::drawPath(QPainter painter, const QVectorQPoint path, int cell, int offsetX, int offsetY) { QPolygon pts; for (const QPoint p : path) { // p.x() 是行号p.y() 是列号映射到像素时必须反过来 pts QPoint(offsetX p.y() * cell cell / 2, offsetY p.x() * cell cell / 2); } QPen pen(QColor(230, 80, 60), cell / 3); painter.setPen(pen); painter.drawPolyline(pts); }路径宽度取cell / 3太宽会把整格盖成红色看不清墙太窄在屏幕上看不清这个比例在 30x30 到 100x100 的迷宫上都成立。起点和终点我习惯再用实心方块标记起点用绿色、终点用蓝色和红色路径区分开painter.fillRect(offsetX, offsetY, cell, cell, QColor(46, 160, 90)); painter.fillRect(offsetX (cols - 1) * cell, offsetY (rows - 1) * cell, cell, cell, QColor(50, 90, 200));绘制路径的数据来自第 4 章的 BFS 函数。在「新建迷宫」的按钮槽里流程是生成迷宫 → 调用 BFS 求路径 → 把路径存到成员变量 path → 调用 update()。在 paintEvent 末尾判断 path 非空就调用 drawPath。这样路径不会在重绘时丢失也不会重新计算。3.3 用 QPixmap 缓冲优化重绘别在 paintEvent 里反复算迷宫paintEvent 在窗口拖动、resize、被其他窗口遮挡恢复时都会被高频调用。如果每次都遍历所有格子并逐条 drawLine100x100 以上的迷宫会明显卡顿。常见做法是把渲染结果先画到一块 QPixmap 上paintEvent 里只做一次 drawPixmap。void MazeWidget::rebuildPixmap() { mazePixmap QPixmap(size()); mazePixmap.fill(Qt::white); QPainter p(mazePixmap); drawMaze(p); // 把 3.1 的画墙逻辑抽成 drawMaze(QPainter) if (!path.isEmpty()) drawPath(p, path, cell, offsetX, offsetY); update(); // 触发 paintEvent 做最后的 blit } void MazeWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.drawPixmap(0, 0, mazePixmap); }生成迷宫、寻路完成后调用 rebuildPixmap而不是在 paintEvent 里重新生成迷宫。resize 时格子大小会变化需要在 resizeEvent 里用新的 width/height 重建 Pixmap而不能让 QPixmap 自动拉伸——自动拉伸会让墙线变粗变糊。桌面演示场景 QPixmap 够用如果要导出高清大图或做像素级操作再换 QImage。注意paintEvent 里不能做耗时操作也不能在里面调用 update()。一旦你习惯在重绘时同步生成迷宫窗口就会卡到看起来像死机这是 Qt 桌面应用新手最常见的性能翻车点。4. 路径获取BFS 与 A* 在迷宫上的落地实现迷宫是典型的无权图每个格子到邻居的代价都是 1。路径获取最常用的两个算法是 BFS 和 A*。BFS 保证最短A* 在大迷宫里更快。两个都值得实现一遍因为它们能互相验证。4.1 BFS 最短路径parent 父指针回溯BFS 从起点逐层扩散第一次到达终点的路径一定最短。实现时最关键的是 parent 数组它记录每个格子是从哪个格子走过来的回溯时沿着 parent 从终点一路回到起点。// 返回从 start 到 end 的格子坐标列表走不通返回空列表 // QPoint 的 x 表示行号y 表示列号 QVectorQPoint bfsShortestPath(const QVectorQVectorCell maze, const QPoint start, const QPoint end) { const int rows maze.size(); const int cols maze[0].size(); // parent 记录每个格子的来源(-1, -1) 表示尚未访问 QVectorQVectorQPoint parent( rows, QVectorQPoint(cols, QPoint(-1, -1))); QQueueQPoint q; q.enqueue(start); parent[start.x()][start.y()] start; while (!q.isEmpty()) { QPoint cur q.dequeue(); if (cur end) break; for (int d 0; d 4; d) { if (maze[cur.x()][cur.y()].walls[d]) continue; // 有墙走不通 int nr cur.x() dr[d]; int nc cur.y() dc[d]; if (nr 0 || nr rows || nc 0 || nc cols) continue; if (parent[nr][nc].x() ! -1) continue; // 已经访问过 parent[nr][nc] cur; q.enqueue({nr, nc}); } } // 从终点沿 parent 回溯得到从起点到终点的正向路径 QVectorQPoint path; QPoint cur end; while (cur ! start) { path.push_front(cur); cur parent[cur.x()][cur.y()]; if (cur.x() -1) return {}; // 终点不可达 } path.push_front(start); return path; }这里有两个容易被新手写成 bug 的点。第一parent 数组既当「来源记录」又当「访问标记」parent[nr][nc].x() ! -1就表示访问过起点要单独设为自身。第二回溯时如果 parent 出现 -1说明终点不在起点所在的连通区域直接返回空列表不要继续循环。BFS 在迷宫上的时间复杂度是 O(rows × cols)500x500 也就是 25 万个格子的规模现代机器毫秒级完成完全可以在主线程直接跑。真正需要注意的是路径回溯顺序用 push_front 把头插到前面得到的 path 才是从起点到终点如果用 push_back 再 reverse 效果一样但头插更直观。4.2 A* 加速曼哈顿启发式与重复入队的处理A* 比 BFS 多一个启发式评估用 f g h 决定优先扩展哪个格子。g 是当前已知最短步数h 是到终点的估计代价。由于迷宫只能上下左右移动最合理的启发式是曼哈顿距离h 行差绝对值 列差绝对值。曼哈顿距离不会高估真实代价满足 A* 的「可采纳性」条件所以第一次从 open 列表弹出终点时找到的一定是最短路径。A* 的经典工程坑在 open 列表同一节点可能被多次发现并多次入队旧记录不会自动作废。如果不处理open 列表会膨胀到离谱A* 反而比 BFS 慢。常见处理是出队时校验这条记录的 f 是否还等于当前 g h不等就丢弃。#include queue // std::priority_queue int manhattan(const QPoint a, const QPoint b) { return qAbs(a.x() - b.x()) qAbs(a.y() - b.y()); } using QueueItem QPairint, QPoint; // first f 值second 格子坐标 QVectorQPoint aStarPath(const QVectorQVectorCell maze, const QPoint start, const QPoint end) { const int rows maze.size(); const int cols maze[0].size(); QVectorQVectorint g(rows, QVectorint(cols, INT_MAX)); QVectorQVectorQPoint parent( rows, QVectorQPoint(cols, QPoint(-1, -1))); auto cmp [](const QueueItem a, const QueueItem b) { return a.first b.first; // 小顶堆f 最小的在堆顶 }; std::priority_queueQueueItem, QVectorQueueItem, decltype(cmp) open(cmp); g[start.x()][start.y()] 0; parent[start.x()][start.y()] start; open.push({manhattan(start, end), start}); while (!open.empty()) { auto [f, cur] open.top(); open.pop(); // 出队校验这条记录的 f 与当前 g h 不一致说明是过期记录 if (f ! g[cur.x()][cur.y()] manhattan(cur, end)) continue; if (cur end) break; for (int d 0; d 4; d) { if (maze[cur.x()][cur.y()].walls[d]) continue; int nr cur.x() dr[d]; int nc cur.y() dc[d]; if (nr 0 || nr rows || nc 0 || nc cols) continue; int ng g[cur.x()][cur.y()] 1; if (ng g[nr][nc]) { g[nr][nc] ng; parent[nr][nc] cur; open.push({ng manhattan({nr, nc}, end), {nr, nc}}); } } } // 回溯路径与 BFS 完全一样 QVectorQPoint path; QPoint cur end; while (cur ! start) { path.push_front(cur); cur parent[cur.x()][cur.y()]; if (cur.x() -1) return {}; } path.push_front(start); return path; }代码里的 structured bindingauto [f, cur]是 C17 语法Qt 6 项目默认支持如果你还在用 C11改成手动取item.first和item.second即可。g 数组初始化为 INT_MAX 表示不可达。每次发现更短路径ng g[nr][nc]才更新并重新入队。出队校验那行不能省没有它老记录还会被弹出同一个格子被重复处理多次最坏情况下 open 列表膨胀到指数级200x200 的迷宫都能卡到怀疑人生。4.3 选型对比小迷宫用 BFS大迷宫用 A*真实工程场景下怎么选直接用这张表场景推荐算法理由100x100 以下的纯演示BFS代码直观、结果必为最短、主线程毫秒级100x100 以上、需要频繁换起点终点A*曼哈顿启发显著缩小搜索范围做搜索过程动画演示BFS逐层扩散的视觉效果最好A* 的「直奔目标」过程也有效但不如 BFS 直观迷宫带权值每步代价不同Dijkstra 或 A* 升级BFS 只适用于每步代价为 1 的情况对多数 Qt 桌面迷宫BFS 已经够用。真正的瓶颈在绘制而不是搜索除非你在同一张 300x300 以上的迷宫里频繁换起点终点A* 的性能优势才体现得明显。交互层面的「路径获取」也可以做成鼠标点选在 mousePressEvent 里把点击位置换算成行列号点一下设起点、点一下设终点然后调用 BFS 重新求路径并 update。换算公式和 3.1 的绘制映射相反row (y - offsetY) / cellcol (x - offsetX) / cell这是复用现有数据结构的常见做法不用新建任何逻辑。5. 随机迷宫常见问题排查五个高频踩坑记录这一章专门记踩坑。每一条都是我在调试迷宫类项目时真实碰到过的现象按「现象 → 原因 → 解决」的顺序写能帮你省下大量排查时间。5.1 生成出的迷宫墙是单边的格子之间走不通现象迷宫生成完有的格子之间明明没有墙但另一个方向看过去墙还在甚至路线完全断裂BFS 返回空路径。原因拆墙时只写了一边。比如从格子 A 往东拆了A.walls[EAST] false但忘了把邻居 B 的B.walls[WEST] false同步拆掉。递归回溯的 visited 标记保证每个格子都被访问过但墙数据不对称寻路时 BFS 查询的是当前格的墙状态遇到这种「单边墙」就以为走不通。解决把拆墙固定成两行同步写一行拆当前格一行拆目标格的反向墙。想快速验证对称性可以写个临时函数遍历每个格子检查maze[r][c].walls[d]是否等于maze[nr][nc].walls[(d2)%4]只要有一处不相等就是拆墙逻辑漏了。5.2 同一个 seed 生成的迷宫每次都不一样现象设置了固定 seed点两次「新建迷宫」两张迷宫完全不同。更让人抓狂的是明明某次生成了张特别难走的迷宫重启程序后就再也复现不出来。原因大概率用了全局随机源。QRandomGenerator::global()每次进程启动的随机序列都不同qrand()更是依赖全局状态固定 seed 只对局部的QRandomGenerator(seed)实例生效。解决把 seed 作为参数传进 generateMaze并把当前 seed 显示在窗口标题或界面标签上。调试期固定 seed42 跑通所有逻辑最后再放开随机。我在模拟项目 X 里就因为「看起来固定了 seed 但实际用的全局源」白调了半小时从那以后生成函数的第一个参数一定是 seed。5.3 路径和墙对不上走出迷宫区域现象红线路径要么斜着穿过墙要么画在迷宫外面迷宫本身看起来是正常的BFS 返回的路径在数值上也能走通但渲染后就是错位。原因坐标约定混用。QPainter 的 x 是横向、y 是纵向而迷宫数组第一维 row 是纵向、第二维 col 是横向。如果绘制时写成offsetX p.x() * cell实际是把行号当成了横向偏移路径自然歪斜。解决渲染映射固定写两行横向取p.y()列号纵向取p.x()行号。排查技巧是打印起点、终点和路径前三个格子的行列号对照迷宫数组肉眼看第 2 行第 3 列像素应该在左侧第三个格子的纵向第二格。一旦「路径与墙对不上」先别怀疑算法先查映射。5.4 迷宫超过 200x200 后界面卡死现象点击生成后窗口转圈好几秒拖动窗口时明显掉帧像假死。原因生成、寻路、绘制全部挤在主线程。生成本身是 O(rows × cols) 的循环绘制又是每格多条 drawLine再加上 paintEvent 在 resize 时被高频触发事件循环被占满界面自然卡死。解决生成和寻路放进 QtConcurrent::run 或 QThread完成后通过信号回到主线程调用 rebuildPixmap。绘制上一定要用 QPixmap 缓冲paintEvent 只做 blit。先做到这两点再把格子数往上加500x500 也能保持流畅。另外注意 release 和 debug 模式性能差异极大测速请用 release 构建否则你会被 debug 的几十倍性能差误导。5.5 A* 比 BFS 还慢open 列表膨胀得离谱现象同样 300x300 迷宫A* 耗时是 BFS 的好几倍调试时打印 open 列表大小发现里面的元素比迷宫总格子数还多。原因两个坑叠加。第一个是启发式选错用了欧氏距离sqrt(dx*dx dy*dy)和上下左右四邻域不匹配搜索范围变大第二个是 open 列表的过期记录没有出队校验同一节点被反复处理列表越积越多。解决h 换成曼哈顿距离并加上 4.2 里的出队校验。排查时在 while 循环里打印open.size()如果它长期大于迷宫总格子数几乎可以断定是过期记录在堆积。追求极致性能时可以把 QPoint 换成row * cols col的整型键减少比较和哈希开销高频调用能省不少时间。6. 进阶玩法搜索动画、键盘闯关与迷宫导出基础功能跑通后这三个小功能能让项目从「作业」变成「作品」。6.1 BFS 事件回放把搜索过程做成逐帧动画BFS 一次算完结果但演示时大家想看它是怎么一点点扩散的。做法是 BFS 里每次发现新格子就记到 history 列表回放时用一个 QTimer 每隔几十毫秒取出一个格子标记为已访问并 update// BFS 入队时记录顺序 history.append({nr, nc}); // 回放定时器 QTimer *timer new QTimer(this); timer-setInterval(30); connect(timer, QTimer::timeout, this, [this]() { if (playIndex history.size()) { timer-stop(); return; } visitedSet.insert(history[playIndex]); update(); });注意 history 记录的是 BFS 的访问顺序天然就是「近处先扩散、远处后扩散」视觉效果非常直观。回放前先把迷宫完整画到底层 QPixmap 上再在半透明图层上叠加 visited 色块。6.2 键盘控制角色手动走迷宫生成完迷宫后用方向键手动走一遍是验证迷宫连通性和墙数据正确性的最直接方式。给角色一个行列坐标在 keyPressEvent 里判断墙void MazeWidget::keyPressEvent(QKeyEvent *event) { int d -1; switch (event-key()) { case Qt::Key_Up: d NORTH; break; case Qt::Key_Down: d SOUTH; break; case Qt::Key_Left: d WEST; break; case Qt::Key_Right: d EAST; break; default: return; } if (d 0 !maze[playerRow][playerCol].walls[d]) { playerRow dr[d]; playerCol dc[d]; update(); } }判断当前格对应方向的墙即可不需要再查目标格的反向墙——生成算法维护了对称性两边的墙状态永远一致。能手动从入口走到出口再谈自动寻路这是我很推荐的验证顺序。6.3 一键导出迷宫 PNGQPixmap 缓冲已经存在导出就是一行的事mazePixmap.save(QString(maze_%1.png).arg(seed));想导出不带路径的迷宫渲染前别调用 drawPath 即可。加个复选框控制「导出带路径 / 不带路径」两张图一起存做算法对比或打印题面都很方便。注意大字谜宫导出前把窗口调到与迷宫比例接近的状态否则 PNG 四周会有大片空白。我自己的习惯是把 seed、算法类型、搜索耗时全部打进窗口标题例如「30x30 | seed 42 | BFS 8ms」这样复现问题、给别人讲方案都省事。某次调试迷宫连通性我排查了半天最后发现是固定种子没生效从此养成了把 seed 常驻 UI 的习惯。这个组合拳做完你的迷宫项目就既能看、能玩、能导出还方便继续扩展成寻路算法对比工具。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

GrabCut MATLAB图像分割实战:参数调优与避坑指南

GrabCut MATLAB图像分割实战:参数调优与避坑指南

简介:这份资源面向计算机视觉初学者与图像处理方向的学生、开发者,提供在MATLAB环境中实现GrabCut图像分割算法的完整代码示例。GrabCut基于马尔科夫随机场与高斯混合模型,通过图割迭代优化前景背景标签,适合需要交互式区域分割、…

2026/10/14 0:13:45 阅读更多 →
谱减法去噪基础版:从原理到代码的语音增强入门实践

谱减法去噪基础版:从原理到代码的语音增强入门实践

音频去噪是个越做越有意思的方向,而且我越来越觉得,入门语音增强最好的路径不是直接堆模型,而是先把经典的谱减法跑通一遍。最近我把“谱减法去噪基础版”从算法到代码完整实现了一遍:读音频、分帧加窗、FFT、噪声估计、谱减、逆变…

2026/10/14 0:13:45 阅读更多 →
QOwnNotes 在 Gentoo Linux 上的安装指南:overlay 仓库配置与密钥环故障排查

QOwnNotes 在 Gentoo Linux 上的安装指南:overlay 仓库配置与密钥环故障排查

桌面应用 【免费下载链接】QOwnNotes QOwnNotes is a plain-text file notepad and todo-list manager with Markdown support and Nextcloud / ownCloud integration. 项目地址: https://gitcode.com/gh_mirrors/qo/QOwnNotes 点击查看 免费下载 QOwnNotes 是一款…

2026/10/14 0:13:45 阅读更多 →

最新新闻

Python第3章练习题解析:数据类型、字符串与格式化输出避坑指南

Python第3章练习题解析:数据类型、字符串与格式化输出避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:07:04 阅读更多 →
ESP32S3 Dev Module:Arduino兼容的原生USB嵌入式开发新范式

ESP32S3 Dev Module:Arduino兼容的原生USB嵌入式开发新范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:07:04 阅读更多 →
高位数阶乘计算:MATLAB、Python、C三语言实现与性能对比

高位数阶乘计算:MATLAB、Python、C三语言实现与性能对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:07:04 阅读更多 →
element-react Radio 单选框组件详解:受控选中、Group 聚合与 Button 样式的源码级解析

element-react Radio 单选框组件详解:受控选中、Group 聚合与 Button 样式的源码级解析

前端UI组件设计系统 【免费下载链接】element-react Element UI 项目地址: https://gitcode.com/gh_mirrors/el/element-react 点击查看 免费下载 本篇技术文章围绕 element-react(Element UI for React,当前仓库版本 1.4.34)的 …

2026/10/14 1:07:04 阅读更多 →
密码与加密技术复习指南:从对称加密到PKI的网络安全知识框架

密码与加密技术复习指南:从对称加密到PKI的网络安全知识框架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:07:04 阅读更多 →
Dart List实战指南:从增删改查到Flutter状态管理踩坑总结

Dart List实战指南:从增删改查到Flutter状态管理踩坑总结

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 1:06:04 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →