2 条题解
-
0
为了方便您构建 OJ(Online Judge)测试数据,我编写了一个符合 NOI 竞赛标准的测试数据生成器。该生成器采用 C++14 编写,包含了完善的 BFS 求解逻辑以生成
.out文件,并设计了 10 个具有代表性的测试点。数据生成器设计思路
- 覆盖性:涵盖了最小迷宫、无出口迷宫、入口在边界、长路径迷宫以及 的最大规模随机迷宫。
- 安全性:求解器使用非递归的
std::queue实现 BFS,有效防止在大规模蛇形路径下出现爆栈。 - 合规性:输入输出格式严格遵守题目描述。文件大小极小(10个点总计约 150KB),远低于 2MB 限制。
C++ 数据生成器源代码
#include <iostream> #include <fstream> #include <vector> #include <string> #include <queue> #include <random> #include <ctime> #include <algorithm> using namespace std; // 方向数组 const int dr[] = {-1, 1, 0, 0}; const int dc[] = {0, 0, -1, 1}; // --- 标准 BFS 求解器 --- int solve(int m, int n, const vector<string>& maze, int r0, int c0) { queue<pair<int, int>> q; vector<vector<int>> dist(m, vector<int>(n, -1)); q.push({r0, c0}); dist[r0][c0] = 0; while (!q.empty()) { pair<int, int> curr = q.front(); q.pop(); for (int i = 0; i < 4; i++) { int nr = curr.first + dr[i]; int nc = curr.second + dc[i]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.' && dist[nr][nc] == -1) { dist[nr][nc] = dist[curr.first][curr.second] + 1; // 判断是否为出口 if (nr == 0 || nr == m - 1 || nc == 0 || nc == n - 1) { if (!(nr == r0 && nc == c0)) { return dist[nr][nc]; } } q.push({nr, nc}); } } } return -1; } // --- 随机工具 --- mt19937 rng(time(0)); int randRange(int l, int r) { return uniform_int_distribution<int>(l, r)(rng); } // --- 测试点生成逻辑 --- void make_test(int t) { string in_name = to_string(t) + ".in"; string out_name = to_string(t) + ".out"; ofstream fin(in_name); int m, n, r0, c0; vector<string> maze; if (t == 1) { // 极小边界: 入口在边界且唯一 m = 1, n = 2; maze = {".+"}; r0 = 0, c0 = 0; } else if (t == 2) { // 入口在边界且被墙包围 m = 3, n = 3; maze = {"+++", "+.+", "+++"}; r0 = 1, n = 3, maze[1][0] = '.'; // 入口在边缘 r0 = 1, c0 = 0; } else if (t == 3) { // 普通 10x10 随机 m = 10, n = 10; for(int i=0; i<m; i++) { string row = ""; for(int j=0; j<n; j++) row += (randRange(0, 10) < 3 ? '+' : '.'); maze.push_back(row); } r0 = randRange(1, m-2), c0 = randRange(1, n-2); maze[r0][c0] = '.'; } else if (t == 4) { // 无出口迷宫 (边界全是墙) m = 50, n = 50; for(int i=0; i<m; i++) { string row(n, '.'); if (i == 0 || i == m-1) fill(row.begin(), row.end(), '+'); row[0] = row[n-1] = '+'; maze.push_back(row); } r0 = 25, c0 = 25; } else if (t == 5) { // 蛇形长路径 m = 100, n = 100; maze.assign(m, string(n, '+')); for(int i=0; i<m; i+=2) { if((i/2)%2 == 0) for(int j=0; j<n-1; j++) maze[i][j] = '.'; else for(int j=1; j<n; j++) maze[i][j] = '.'; if(i+1 < m) { if((i/2)%2 == 0) maze[i+1][n-2] = '.'; else maze[i+1][1] = '.'; } } r0 = 0, c0 = 0; } else { // 最大规模随机 m = 100, n = 100; for(int i=0; i<m; i++) { string row = ""; for(int j=0; j<n; j++) row += (randRange(0, 100) < 25 ? '+' : '.'); maze.push_back(row); } r0 = randRange(0, m-1), c0 = randRange(0, n-1); maze[r0][c0] = '.'; } // 输出输入文件 fin << m << " " << n << "\n"; for(const string& s : maze) fin << s << "\n"; fin << r0 << " " << c0 << "\n"; fin.close(); // 计算答案并输出 int res = solve(m, n, maze, r0, c0); ofstream fout(out_name); fout << res << endl; fout.close(); cout << "Testcase " << t << " generated. Size: " << m << "x" << n << " Result: " << res << endl; } int main() { for (int i = 1; i <= 10; i++) { make_test(i); } return 0; }测试点详细说明
测试点 规模 场景描述 考查点 1 极小迷宫,入口即边界 出口判定逻辑(入口不可作为出口) 2 入口在边界且不连通 基础搜索及 -1 的判定 3 普通中小型迷宫 基础 BFS 逻辑 4 边界全部封死 复杂不连通图处理 5 蛇形走廊结构 考查最长路径下的 BFS 效率 6 棋盘格分布墙壁 考查搜索路径的选择性 7-9 高密度/低密度随机 综合压力测试与性能考查 10 大型稀疏迷宫 考查多出口情况下的最短路选择 使用方法
- 将代码复制到本地
gen.cpp。 - 使用
g++ gen.cpp -o gen -std=c++14 -O2编译。 - 运行
./gen。当前目录下将生成1.in/1.out到10.in/10.out。
教练寄语
- 空间优化:在处理 矩阵时,静态数组
int dist[105][105]仅占用约 40KB,不需要使用std::vector。 - 防止死循环:在生成随机迷宫时,务必保证入口坐标处被强制修改为
.。 - 特判逻辑:在 BFS 中,判断“是出口”的代码应放在“新位置入队”时,这样可以提前结束搜索,提高效率。但在本题规模下, 已经足够快。
-
0
在 NOI 竞赛中,求迷宫/网格图的最短路径,广度优先搜索 (BFS) 是标准且唯一的 正解。DFS 在此类问题中效率极低(指数级复杂度),不适合作为“逐渐过渡”的解法。
因此,这里的过渡将侧重于:从“工程化/易读的 STL 写法”过渡到“竞赛化/高性能的静态数组写法”。
版本一:标准 STL 写法 (适合初学者,逻辑清晰)
特点:使用
vector,string,queue等 STL 容器,代码可读性高,不易发生内存越界,适合 的数据。/** * 版本一:基于 STL 的标准 BFS * 适合:NOIP 普及组/提高组,数据规模中等 * 优点:代码逻辑清晰,不易写挂 */ #include <iostream> #include <vector> #include <string> #include <queue> using namespace std; // 方向数组:上、下、左、右 const int dr[] = {-1, 1, 0, 0}; const int dc[] = {0, 0, -1, 1}; int main() { // 1. IO 提速 ios::sync_with_stdio(false); cin.tie(0); int m, n; if (!(cin >> m >> n)) return 0; // 2. 读入迷宫 vector<string> maze(m); for (int i = 0; i < m; i++) { cin >> maze[i]; } int r0, c0; cin >> r0 >> c0; // 3. 初始化距离数组 (dist) // -1 代表未访问,>=0 代表步数 // 空间复杂度 O(MN) vector<vector<int>> dist(m, vector<int>(n, -1)); // 4. BFS 队列 queue<pair<int, int>> q; // 初始状态入队 q.push({r0, c0}); dist[r0][c0] = 0; // 起点步数为0 while (!q.empty()) { pair<int, int> curr = q.front(); q.pop(); int r = curr.first; int c = curr.second; // 尝试 4 个方向 for (int i = 0; i < 4; i++) { int nr = r + dr[i]; int nc = c + dc[i]; // --- 核心逻辑:严密的“安检”顺序 --- // [安检1] 越界检查 if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue; // [安检2] 墙壁检查 if (maze[nr][nc] == '+') continue; // [安检3] 判重检查 (是否已访问) if (dist[nr][nc] != -1) continue; // --- 通过安检,更新状态 --- // 更新距离 dist[nr][nc] = dist[r][c] + 1; // [安检4] 目标判定 (是否到达有效出口) // 条件:位于边界 if (nr == 0 || nr == m - 1 || nc == 0 || nc == n - 1) { // 条件:不能是入口本身 (虽然入口dist为0,新点dist>0,逻辑上不会重合,但判断坐标更直观) if (!(nr == r0 && nc == c0)) { cout << dist[nr][nc] << endl; return 0; // 找到最近出口,直接结束 } } // 加入队列,继续扩散 q.push({nr, nc}); } } // 队列空了还没找到 cout << -1 << endl; return 0; }
版本二:竞赛极致优化版 (静态数组 + 手写队列)
特点:
- 静态数组:避免
vector动态开辟内存的开销。 - 手写队列:避免
std::queue的封装开销,内存连续性更好。 - IO 优化:使用
scanf/printf,在数据量极大时优势明显。
适用场景:省选/NOI 级别,或时间/空间限制极其严格的题目(如 )。
/** * 版本二:竞赛级静态数组优化 BFS * 适合:卡常数、内存限制严格、大规模数据 */ #include <cstdio> using namespace std; // 定义最大范围,稍微开大一点防止越界 const int MAXN = 105; // 全局静态数组,存储在堆区(Data Segment),不占用栈空间 char maze[MAXN][MAXN]; int dist[MAXN][MAXN]; // 手写结构体代替 pair struct Node { int r, c; }; // 手写队列:数组 + 头尾指针 Node q[MAXN * MAXN]; int head = 0, tail = 0; const int dr[] = {-1, 1, 0, 0}; const int dc[] = {0, 0, -1, 1}; int main() { int m, n; // scanf 返回值检查是好习惯 if (scanf("%d %d", &m, &n) == EOF) return 0; for (int i = 0; i < m; i++) { scanf("%s", maze[i]); } int r0, c0; scanf("%d %d", &r0, &c0); // 初始化 dist 数组为 -1 // 可以用 memset(dist, -1, sizeof(dist)); 但需注意头文件 <cstring> // 手写循环更通用 for(int i = 0; i < m; i++) for(int j = 0; j < n; j++) dist[i][j] = -1; // 初始入队 q[tail++] = {r0, c0}; // 入队操作:赋值后 tail++ dist[r0][c0] = 0; while (head < tail) { // 队列不为空条件 Node curr = q[head++]; // 出队操作:取值后 head++ int r = curr.r; int c = curr.c; for (int i = 0; i < 4; i++) { int nr = r + dr[i]; int nc = c + dc[i]; // 1. 越界 if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue; // 2. 墙 if (maze[nr][nc] == '+') continue; // 3. 已访问 if (dist[nr][nc] != -1) continue; // 更新步数 dist[nr][nc] = dist[r][c] + 1; // 4. 出口判定 (及早退出优化) if (nr == 0 || nr == m - 1 || nc == 0 || nc == n - 1) { if (!(nr == r0 && nc == c0)) { printf("%d\n", dist[nr][nc]); return 0; } } // 入队 q[tail++] = {nr, nc}; } } printf("-1\n"); return 0; }
复杂度分析与思考过程
1. 时间复杂度
- 思考:BFS 的本质是遍历图。在这个网格图中,节点数 ,边数 (每个格子连4个邻居)。
- 过程:每个有效格子最多进队一次,也最多出队一次。每次处理涉及 4 次常数级别的检查。
- 结论:。
- 优化建议:
- 及早判定 (Early Exit):在生成新节点
(nr, nc)时立即检查它是否是出口,如果是立刻返回。不要等到它出队时再检查。这可以节省最后一层的所有入队出队开销。 - 分支预测:将最可能发生的情况放在前面判断(通常越界判断必须在最前)。
- 及早判定 (Early Exit):在生成新节点
2. 空间复杂度
- 思考:我们需要存什么?
- 迷宫本身
maze。 - 记录步数和是否访问过的
dist。 - BFS 队列
q。
- 迷宫本身
- 计算:
maze: 字节。dist: 字节 (int)。q: 最坏情况(如蛇形全图)需要存 个点。
- 结论:。
- 优化建议:
- 复用内存:如果内存限制极其变态(如 1MB),可以直接在
maze数组上标记已访问(例如把.改成+),并在队列结构体中多存一个step变量,从而省去dist数组。但在 NOI 正常题目中,空间换时间是主流,保留dist数组更直观。
- 复用内存:如果内存限制极其变态(如 1MB),可以直接在
关键点与易错点总结 (教练笔记)
-
逻辑穿透 (Critical):
- 错误写法:先判断
if(是边界) return success;,后判断if(是墙) continue;。 - 后果:会“穿墙”,如果边界是墙,会被错误地当成出口。
- 口诀:先判合法性(越界、墙、已访问),再判目标(是否为出口)。
- 错误写法:先判断
-
入口非出口 (The "Entrance Trap"):
- 题目明确规定:出口不能是入口。
- 如果不加
!(nr == r0 && nc == c0)判断,当入口位于边界时(如样例 3),程序会瞬间输出 0 或 1(取决于判断时机),导致 WA。
-
队列操作 (Queue Logic):
- 入队即标记:必须在
q.push()之前或同时更新dist。如果在pop()时才更新dist,会导致同一个点被多个邻居重复加入队列,导致内存爆炸 (MLE) 或超时 (TLE)。
- 入队即标记:必须在
-
方向数组 (Direction Array):
- 初学者容易写很多
if-else来处理上下左右,容易写错坐标。使用dr/dc数组循环处理是 NOI 的基本素养。
- 初学者容易写很多
- 静态数组:避免
- 1
信息
- ID
- 19458
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 4
- 已通过
- 1
- 上传者